diff --git a/alembic/versions/p9c1d2e3f4a5_make_contact_role_nullable.py b/alembic/versions/p9c1d2e3f4a5_make_contact_role_nullable.py deleted file mode 100644 index fb53b64d..00000000 --- a/alembic/versions/p9c1d2e3f4a5_make_contact_role_nullable.py +++ /dev/null @@ -1,29 +0,0 @@ -"""make contact role nullable - -Revision ID: p9c1d2e3f4a5 -Revises: o8b9c0d1e2f3 -Create Date: 2026-03-11 10:30:00.000000 -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision: str = "p9c1d2e3f4a5" -down_revision: Union[str, Sequence[str], None] = "o8b9c0d1e2f3" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.alter_column( - "contact", "role", existing_type=sa.String(length=100), nullable=True - ) - - -def downgrade() -> None: - op.alter_column( - "contact", "role", existing_type=sa.String(length=100), nullable=False - ) diff --git a/alembic/versions/q0d1e2f3a4b5_make_contact_type_nullable.py b/alembic/versions/q0d1e2f3a4b5_make_contact_type_nullable.py deleted file mode 100644 index 3923139e..00000000 --- a/alembic/versions/q0d1e2f3a4b5_make_contact_type_nullable.py +++ /dev/null @@ -1,35 +0,0 @@ -"""make contact type nullable - -Revision ID: q0d1e2f3a4b5 -Revises: p9c1d2e3f4a5 -Create Date: 2026-03-11 17:10:00.000000 -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision: str = "q0d1e2f3a4b5" -down_revision: Union[str, Sequence[str], None] = "p9c1d2e3f4a5" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.alter_column( - "contact", - "contact_type", - existing_type=sa.String(length=100), - nullable=True, - ) - - -def downgrade() -> None: - op.alter_column( - "contact", - "contact_type", - existing_type=sa.String(length=100), - nullable=False, - ) diff --git a/db/contact.py b/db/contact.py index e30b5f57..0fb59473 100644 --- a/db/contact.py +++ b/db/contact.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from typing import List, TYPE_CHECKING, Optional +from typing import List, TYPE_CHECKING from sqlalchemy import Integer, ForeignKey, String, UniqueConstraint from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy @@ -49,8 +49,8 @@ class ThingContactAssociation(Base, AutoBaseMixin): class Contact(Base, AutoBaseMixin, ReleaseMixin, NotesMixin): name: Mapped[str] = mapped_column(String(100), nullable=True) organization: Mapped[str] = lexicon_term(nullable=True) - role: Mapped[Optional[str]] = lexicon_term(nullable=True) - contact_type: Mapped[Optional[str]] = lexicon_term(nullable=True) + role: Mapped[str] = lexicon_term(nullable=False) + contact_type: Mapped[str] = lexicon_term(nullable=False) # primary keys of the nm aquifer tables from which the contacts originate nma_pk_owners: Mapped[str] = mapped_column(String(100), nullable=True) diff --git a/schemas/contact.py b/schemas/contact.py index 29eaad45..d6fe28a0 100644 --- a/schemas/contact.py +++ b/schemas/contact.py @@ -150,8 +150,8 @@ class CreateContact(BaseCreateModel, ValidateContact): thing_id: int name: str | None = None organization: str | None = None - role: Role | None = None - contact_type: ContactType | None = None + role: Role + contact_type: ContactType nma_pk_owners: str | None = None # description: str | None = None # email: str | None = None @@ -218,8 +218,8 @@ class ContactResponse(BaseResponseModel): name: str | None organization: str | None - role: Role | None - contact_type: ContactType | None + role: Role + contact_type: ContactType incomplete_nma_phones: List[str] = [] emails: List[EmailResponse] = [] phones: List[PhoneResponse] = [] diff --git a/schemas/well_inventory.py b/schemas/well_inventory.py index 75d3edc3..05544629 100644 --- a/schemas/well_inventory.py +++ b/schemas/well_inventory.py @@ -375,6 +375,8 @@ def validate_model(self): key = f"contact_{jdx}" name = getattr(self, f"{key}_name") organization = getattr(self, f"{key}_organization") + role = getattr(self, f"{key}_role") + contact_type = getattr(self, f"{key}_type") # Treat name or organization as contact data too, so bare contacts # still go through the same cross-field rules as fully populated ones. @@ -399,6 +401,14 @@ def validate_model(self): raise ValueError( f"At least one of {key}_name or {key}_organization must be provided" ) + if not role: + raise ValueError( + f"{key}_role is required when contact data is provided" + ) + if not contact_type: + raise ValueError( + f"{key}_type is required when contact data is provided" + ) for idx in (1, 2): if any(getattr(self, f"{key}_address_{idx}_{a}") for a in all_attrs): if not all( diff --git a/services/well_inventory_csv.py b/services/well_inventory_csv.py index a2ca44f0..1eb2bac2 100644 --- a/services/well_inventory_csv.py +++ b/services/well_inventory_csv.py @@ -353,21 +353,12 @@ def _make_contact(model: WellInventoryRow, well: Thing, idx) -> dict: "address_type": address_type, } ) - return { "thing_id": well.id, "name": name, "organization": organization, - "role": ( - getattr(model, f"contact_{idx}_role").value - if hasattr(getattr(model, f"contact_{idx}_role"), "value") - else getattr(model, f"contact_{idx}_role") - ), - "contact_type": ( - getattr(model, f"contact_{idx}_type").value - if hasattr(getattr(model, f"contact_{idx}_type"), "value") - else getattr(model, f"contact_{idx}_type") - ), + "role": getattr(model, f"contact_{idx}_role").value, + "contact_type": getattr(model, f"contact_{idx}_type").value, "emails": emails, "phones": phones, "addresses": addresses, diff --git a/tests/features/data/well-inventory-missing-contact-role.csv b/tests/features/data/well-inventory-missing-contact-role.csv index e5948aa9..a053650d 100644 --- a/tests/features/data/well-inventory-missing-contact-role.csv +++ b/tests/features/data/well-inventory-missing-contact-role.csv @@ -1,3 +1,3 @@ project,well_name_point_id,site_name,date_time,field_staff,utm_easting,utm_northing,utm_zone,elevation_ft,elevation_method,measuring_point_height_ft,field_staff_2,field_staff_3,contact_1_name,contact_1_organization,contact_1_role,contact_1_type,contact_1_phone_1,contact_1_phone_1_type,contact_1_phone_2,contact_1_phone_2_type,contact_1_email_1,contact_1_email_1_type,contact_1_email_2,contact_1_email_2_type,contact_1_address_1_line_1,contact_1_address_1_line_2,contact_1_address_1_type,contact_1_address_1_state,contact_1_address_1_city,contact_1_address_1_postal_code,contact_1_address_2_line_1,contact_1_address_2_line_2,contact_1_address_2_type,contact_1_address_2_state,contact_1_address_2_city,contact_1_address_2_postal_code,contact_2_name,contact_2_organization,contact_2_role,contact_2_type,contact_2_phone_1,contact_2_phone_1_type,contact_2_phone_2,contact_2_phone_2_type,contact_2_email_1,contact_2_email_1_type,contact_2_email_2,contact_2_email_2_type,contact_2_address_1_line_1,contact_2_address_1_line_2,contact_2_address_1_type,contact_2_address_1_state,contact_2_address_1_city,contact_2_address_1_postal_code,contact_2_address_2_line_1,contact_2_address_2_line_2,contact_2_address_2_type,contact_2_address_2_state,contact_2_address_2_city,contact_2_address_2_postal_code,directions_to_site,specific_location_of_well,repeat_measurement_permission,sampling_permission,datalogger_installation_permission,public_availability_acknowledgement,result_communication_preference,contact_special_requests_notes,ose_well_record_id,date_drilled,completion_source,total_well_depth_ft,historic_depth_to_water_ft,depth_source,well_pump_type,well_pump_depth_ft,is_open,datalogger_possible,casing_diameter_ft,measuring_point_description,well_purpose,well_purpose_2,well_status,monitoring_frequency,sampling_scenario_notes,well_measuring_notes,sample_possible -Middle Rio Grande Groundwater Monitoring,MRG-001_MP1,Smith Farm Domestic Well,2025-02-15T10:30:00,A Lopez,250000,4000000,13N,5250,Survey-grade GPS,1.5,B Chen,,John Smith,NMBGMR,,Primary,505-555-0101,Primary,,,john.smith@example.com,Primary,,,123 County Rd 7,,Mailing,NM,Los Lunas,87031,,,,,,,Maria Garcia,NMBGMR,Principal Investigator,Secondary,505-555-0123,Home,,,maria.garcia@mrgcd.nm.gov,Work,,,1931 2nd St SW,Suite 200,Mailing,NM,Albuquerque,87102,,,,,,,Gate off County Rd 7 0.4 miles south of canal crossing,Domestic well in pump house east of residence,True,True,True,True,email,Call before visits during irrigation season,OSE-123456,2010-06-15,Interpreted fr geophys logs by source agency,280,45,"Memory of owner, operator, driller",Submersible,200,True,True,0.5,Top of steel casing inside pump house marked with orange paint,Domestic,,"Active, pumping well",Biannual,Sample only when pump has been off more than 12 hours,Measure before owner starts irrigation,True +Middle Rio Grande Groundwater Monitoring,MRG-001_MP1,Smith Farm Domestic Well,2025-02-15T10:30:00,A Lopez,250000,4000000,13N,5250,Survey-grade GPS,1.5,B Chen,,John Smith No Role,NMBGMR,,Primary,505-555-0101,Primary,,,john.smith@example.com,Primary,,,123 County Rd 7,,Mailing,NM,Los Lunas,87031,,,,,,,Maria Garcia,NMBGMR,Principal Investigator,Secondary,505-555-0123,Home,,,maria.garcia@mrgcd.nm.gov,Work,,,1931 2nd St SW,Suite 200,Mailing,NM,Albuquerque,87102,,,,,,,Gate off County Rd 7 0.4 miles south of canal crossing,Domestic well in pump house east of residence,True,True,True,True,email,Call before visits during irrigation season,OSE-123456,2010-06-15,Interpreted fr geophys logs by source agency,280,45,"Memory of owner, operator, driller",Submersible,200,True,True,0.5,Top of steel casing inside pump house marked with orange paint,Domestic,,"Active, pumping well",Biannual,Sample only when pump has been off more than 12 hours,Measure before owner starts irrigation,True Middle Rio Grande Groundwater Monitoring,MRG-003_MP1,Old Orchard Well,2025-01-20T09:00:00,B Chen,250000,4000000,13N,5320,Global positioning system (GPS),1.8,,,David Emily,NMBGMR,Biologist,Primary,505-555-0303,Work,,,emily.davis@example.org,Work,,,78 Orchard Ln,,Mailing,NM,Los Lunas,87031,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,From Main St turn east on Orchard Ln well house at dead end,Abandoned irrigation well in small cinderblock building,False,False,False,True,phone,Owner prefers weekday visits,,1965-04-10,From driller's log or well report,350,60,"Reported by person other than driller owner agency",Jet,280,False,False,0.75,Top of steel casing under removable hatch use fixed reference mark,Irrigation,,Abandoned,Annual,Sampling not permitted water level only when owner present,Well house can be locked coordinate ahead,False diff --git a/tests/features/data/well-inventory-missing-contact-type.csv b/tests/features/data/well-inventory-missing-contact-type.csv index 6fd4cddc..d3b41faa 100644 --- a/tests/features/data/well-inventory-missing-contact-type.csv +++ b/tests/features/data/well-inventory-missing-contact-type.csv @@ -1,3 +1,3 @@ project,well_name_point_id,site_name,date_time,field_staff,utm_easting,utm_northing,utm_zone,elevation_ft,elevation_method,measuring_point_height_ft,field_staff_2,field_staff_3,contact_1_name,contact_1_organization,contact_1_role,contact_1_type,contact_1_phone_1,contact_1_phone_1_type,contact_1_phone_2,contact_1_phone_2_type,contact_1_email_1,contact_1_email_1_type,contact_1_email_2,contact_1_email_2_type,contact_1_address_1_line_1,contact_1_address_1_line_2,contact_1_address_1_type,contact_1_address_1_state,contact_1_address_1_city,contact_1_address_1_postal_code,contact_1_address_2_line_1,contact_1_address_2_line_2,contact_1_address_2_type,contact_1_address_2_state,contact_1_address_2_city,contact_1_address_2_postal_code,contact_2_name,contact_2_organization,contact_2_role,contact_2_type,contact_2_phone_1,contact_2_phone_1_type,contact_2_phone_2,contact_2_phone_2_type,contact_2_email_1,contact_2_email_1_type,contact_2_email_2,contact_2_email_2_type,contact_2_address_1_line_1,contact_2_address_1_line_2,contact_2_address_1_type,contact_2_address_1_state,contact_2_address_1_city,contact_2_address_1_postal_code,contact_2_address_2_line_1,contact_2_address_2_line_2,contact_2_address_2_type,contact_2_address_2_state,contact_2_address_2_city,contact_2_address_2_postal_code,directions_to_site,specific_location_of_well,repeat_measurement_permission,sampling_permission,datalogger_installation_permission,public_availability_acknowledgement,result_communication_preference,contact_special_requests_notes,ose_well_record_id,date_drilled,completion_source,total_well_depth_ft,historic_depth_to_water_ft,depth_source,well_pump_type,well_pump_depth_ft,is_open,datalogger_possible,casing_diameter_ft,measuring_point_description,well_purpose,well_purpose_2,well_status,monitoring_frequency,sampling_scenario_notes,well_measuring_notes,sample_possible -Middle Rio Grande Groundwater Monitoring,MRG-001_MP1,Smith Farm Domestic Well,2025-02-15T10:30:00,A Lopez,250000,4000000,13N,5250,Survey-grade GPS,1.5,B Chen,,John Smith,NMBGMR,Owner,,505-555-0101,Primary,,,john.smith@example.com,Primary,,,123 County Rd 7,,Mailing,NM,Los Lunas,87031,,,,,,,Maria Garcia,NMBGMR,Principal Investigator,Secondary,505-555-0123,Home,,,maria.garcia@mrgcd.nm.gov,Work,,,1931 2nd St SW,Suite 200,Mailing,NM,Albuquerque,87102,,,,,,,Gate off County Rd 7 0.4 miles south of canal crossing,Domestic well in pump house east of residence,True,True,True,True,email,Call before visits during irrigation season,OSE-123456,2010-06-15,From driller's log or well report,280,45,"Memory of owner, operator, driller",submersible,200,True,True,0.5,Top of steel casing inside pump house marked with orange paint,Domestic,,"Active, pumping well",Biannual,Sample only when pump has been off more than 12 hours,Measure before owner starts irrigation,True +Middle Rio Grande Groundwater Monitoring,MRG-001_MP1,Smith Farm Domestic Well,2025-02-15T10:30:00,A Lopez,250000,4000000,13N,5250,Survey-grade GPS,1.5,B Chen,,John Smith No Type,NMBGMR,Owner,,505-555-0101,Primary,,,john.smith@example.com,Primary,,,123 County Rd 7,,Mailing,NM,Los Lunas,87031,,,,,,,Maria Garcia,NMBGMR,Principal Investigator,Secondary,505-555-0123,Home,,,maria.garcia@mrgcd.nm.gov,Work,,,1931 2nd St SW,Suite 200,Mailing,NM,Albuquerque,87102,,,,,,,Gate off County Rd 7 0.4 miles south of canal crossing,Domestic well in pump house east of residence,True,True,True,True,email,Call before visits during irrigation season,OSE-123456,2010-06-15,From driller's log or well report,280,45,"Memory of owner, operator, driller",submersible,200,True,True,0.5,Top of steel casing inside pump house marked with orange paint,Domestic,,"Active, pumping well",Biannual,Sample only when pump has been off more than 12 hours,Measure before owner starts irrigation,True Middle Rio Grande Groundwater Monitoring,MRG-003_MP1,Old Orchard Well,2025-01-20T09:00:00,B Chen,250000,4000000,13N,5320,Global positioning system (GPS),1.8,,,Emily Davis,NMBGMR,Biologist,Primary,505-555-0303,Work,,,emily.davis@example.org,Work,,,78 Orchard Ln,,Mailing,NM,Los Lunas,87031,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,From Main St turn east on Orchard Ln well house at dead end,Abandoned irrigation well in small cinderblock building,False,False,False,True,phone,Owner prefers weekday visits,,1965-04-10,From driller's log or well report,350,60,"From driller's log or well report",Line Shaft,280,False,False,0.75,Top of steel casing under removable hatch use fixed reference mark,Irrigation,,Abandoned,Annual,Sampling not permitted water level only when owner present,Well house can be locked coordinate ahead,False diff --git a/tests/features/environment.py b/tests/features/environment.py index 9813c38f..5e1e32b9 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -17,6 +17,11 @@ import random from datetime import datetime, timedelta +# Lock test database before any db module imports +# Ensures BDD tests only use ocotilloapi_test, never ocotilloapi_dev +os.environ["POSTGRES_DB"] = "ocotilloapi_test" +os.environ["POSTGRES_PORT"] = "5432" + from alembic import command from alembic.config import Config from sqlalchemy import select diff --git a/tests/features/steps/cli_common.py b/tests/features/steps/cli_common.py index 1483db09..03b8077a 100644 --- a/tests/features/steps/cli_common.py +++ b/tests/features/steps/cli_common.py @@ -62,7 +62,7 @@ def step_impl_command_exit_zero(context): @then("the command exits with a non-zero exit code") def step_impl_command_exit_nonzero(context): - assert context.cli_result.exit_code != 0 + assert context.cli_result.exit_code != 0, context.cli_result.exit_code # ============= EOF ============================================= diff --git a/tests/features/steps/well-inventory-csv-validation-error.py b/tests/features/steps/well-inventory-csv-validation-error.py index 6714acb3..492af59c 100644 --- a/tests/features/steps/well-inventory-csv-validation-error.py +++ b/tests/features/steps/well-inventory-csv-validation-error.py @@ -187,13 +187,26 @@ def step_then_the_response_includes_a_validation_error_indicating_the_invalid_em @then( - 'the response includes a validation error indicating the missing "contact_type" value' + 'the response includes a validation error indicating the missing "contact_role" value' ) def step_step_step_8(context): expected_errors = [ { "field": "composite field error", - "error": "Value error, contact_1_type is required when contact fields are provided", + "error": "Value error, contact_1_role is required when contact data is provided", + } + ] + _handle_validation_error(context, expected_errors) + + +@then( + 'the response includes a validation error indicating the missing "contact_type" value' +) +def step_step_step_9(context): + expected_errors = [ + { + "field": "composite field error", + "error": "Value error, contact_1_type is required when contact data is provided", } ] _handle_validation_error(context, expected_errors) diff --git a/tests/features/well-inventory-csv.feature b/tests/features/well-inventory-csv.feature index 8a1b67ef..ee094ef2 100644 --- a/tests/features/well-inventory-csv.feature +++ b/tests/features/well-inventory-csv.feature @@ -223,19 +223,21 @@ Feature: Bulk upload well inventory from CSV via CLI And the response includes a validation error indicating the invalid email format And 1 well is imported - @positive @validation @BDMS-TBD - Scenario: Upload succeeds when a row has a contact without a contact_role + @negative @validation @BDMS-TBD + Scenario: Upload fails when a row has a contact without a "contact_role" Given my CSV file contains a row with a contact but is missing the required "contact_role" field for that contact When I run the well inventory bulk upload command - Then the command exits with code 0 - And all wells are imported + Then the command exits with a non-zero exit code + And the response includes a validation error indicating the missing "contact_role" value + And 1 well is imported - @positive @validation @BDMS-TBD - Scenario: Upload succeeds when a row has a contact without a "contact_type" + @negative @validation @BDMS-TBD + Scenario: Upload fails when a row has a contact without a "contact_type" Given my CSV file contains a row with a contact but is missing the required "contact_type" field for that contact When I run the well inventory bulk upload command - Then the command exits with code 0 - And all wells are imported + Then the command exits with a non-zero exit code + And the response includes a validation error indicating the missing "contact_type" value + And 1 well is imported @negative @validation @BDMS-TBD Scenario: Upload fails when a row has a contact with an invalid "contact_type" diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index dd7ccdcc..9c13d734 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -641,18 +641,18 @@ def test_upload_invalid_boolean_value(self): assert result.exit_code == 1 def test_upload_missing_contact_type(self): - """Upload succeeds when contact is provided without contact_type.""" + """Upload fails when contact is provided without contact_type.""" file_path = Path("tests/features/data/well-inventory-missing-contact-type.csv") if file_path.exists(): result = well_inventory_csv(file_path) - assert result.exit_code == 0 + assert result.exit_code == 1 def test_upload_missing_contact_role(self): - """Upload succeeds when contact is provided without role.""" + """Upload fails when contact is provided without role.""" file_path = Path("tests/features/data/well-inventory-missing-contact-role.csv") if file_path.exists(): result = well_inventory_csv(file_path) - assert result.exit_code == 0 + assert result.exit_code == 1 def test_upload_partial_water_level_fields(self): """Upload fails when only some water level fields are provided."""