A trigger message validates the fields each fired Abri flow needs 🟥

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-07 10:04:55 +00:00
parent 6a580d7691
commit c281456164
4 changed files with 131 additions and 0 deletions

View file

View file

@ -0,0 +1,28 @@
"""The message contract between the HubSpot scraper and the abri lambda.
One message per scrape, naming every Abri flow that fired plus the deal
fields those flows need. A message missing a field its flow needs fails
validation the resulting failed task / DLQ entry is the agreed
visibility surface.
"""
from datetime import date
from typing import Any, List, Literal, Optional
from pydantic import BaseModel, ConfigDict
AbriFlow = Literal["amend_job", "log_job", "sync_tenant_data"]
class AbriTriggerRequest(BaseModel):
model_config = ConfigDict(extra="ignore")
hubspot_deal_id: str
flows: List[AbriFlow]
place_ref: Optional[str] = None
deal_name: Optional[str] = None
confirmed_survey_date: Optional[date] = None
confirmed_survey_time: Optional[str] = None
def model_post_init(self, __context: Any) -> None:
raise NotImplementedError

View file

View file

@ -0,0 +1,103 @@
from datetime import date
from typing import Any, Dict
import pytest
from pydantic import ValidationError
from applications.abri.abri_trigger_request import AbriTriggerRequest
def _full_message() -> Dict[str, Any]:
return {
"hubspot_deal_id": "9876543210",
"flows": ["amend_job", "log_job", "sync_tenant_data"],
"place_ref": "1007165",
"deal_name": "49 Admers Crescent",
"confirmed_survey_date": "2026-06-24",
"confirmed_survey_time": "14:30",
}
def test_a_full_message_parses_into_a_typed_request() -> None:
# Act
request = AbriTriggerRequest.model_validate(_full_message())
# Assert
assert request.hubspot_deal_id == "9876543210"
assert request.flows == ["amend_job", "log_job", "sync_tenant_data"]
assert request.place_ref == "1007165"
assert request.deal_name == "49 Admers Crescent"
assert request.confirmed_survey_date == date(2026, 6, 24)
assert request.confirmed_survey_time == "14:30"
@pytest.mark.parametrize(
("flow", "missing_field"),
[
("log_job", "place_ref"),
("log_job", "deal_name"),
("log_job", "confirmed_survey_date"),
("amend_job", "confirmed_survey_date"),
("sync_tenant_data", "place_ref"),
],
)
def test_a_message_missing_a_field_its_flow_needs_fails_validation(
flow: str, missing_field: str
) -> None:
# Arrange
message = _full_message()
message["flows"] = [flow]
del message[missing_field]
# Act / Assert
with pytest.raises(ValidationError, match=missing_field):
AbriTriggerRequest.model_validate(message)
def test_a_message_missing_only_fields_of_unfired_flows_parses() -> None:
# Arrange: tenant sync needs only the place_ref (and the deal id).
message = {
"hubspot_deal_id": "9876543210",
"flows": ["sync_tenant_data"],
"place_ref": "1007165",
}
# Act
request = AbriTriggerRequest.model_validate(message)
# Assert
assert request.flows == ["sync_tenant_data"]
assert request.confirmed_survey_date is None
def test_a_message_without_a_confirmed_time_parses_for_logging() -> None:
# Arrange: the time slot is optional — no time means an all-day slot.
message = _full_message()
message["flows"] = ["log_job"]
del message["confirmed_survey_time"]
# Act
request = AbriTriggerRequest.model_validate(message)
# Assert
assert request.confirmed_survey_time is None
def test_a_message_naming_an_unknown_flow_fails_validation() -> None:
# Arrange
message = _full_message()
message["flows"] = ["cancel_job"]
# Act / Assert
with pytest.raises(ValidationError):
AbriTriggerRequest.model_validate(message)
def test_a_message_naming_no_flows_fails_validation() -> None:
# Arrange
message = _full_message()
message["flows"] = []
# Act / Assert
with pytest.raises(ValidationError):
AbriTriggerRequest.model_validate(message)