Repository navigation
feat(fsma): Facility phone for 1.1310 location descriptions (A-2190) #3771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7fd195d
b2ca5c1
0ce96f5
f8b7e68
d3163e8
59df0db
8100dc5
739cac7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """v142 -- supplier_facilities.phone for 21 CFR 1.1310 (A-2190). | ||
|
|
||
| Revision ID: e5cd73f8d1bb | ||
| Revises: 08c7cf36ddbe (v141 traceability_events_append_only) | ||
| Create Date: 2026-10-31 | ||
|
|
||
| Adds an optional business phone on supplier_facilities so a Facility location | ||
| description can carry name + phone + address (or a GLN stand-in). Nullable so | ||
| existing rows stay valid; UI and API accept phone on create. | ||
|
|
||
| Additive. Synthetic fixtures only until SEL-590 clears. | ||
|
|
||
| Note: #3717's fsma.traceability_events append-only trigger landed on main as | ||
| v141 (08c7cf36ddbe); this migration chains after it and does not vendor it. | ||
| Earlier, shipping it as a side-effect of the phone | ||
| column caused Live DB suite CheckViolation failures on intentional tamper | ||
| UPDATEs (append-only policy), which phone ADD COLUMN never touches. | ||
| """ | ||
|
|
||
| from typing import Sequence, Union | ||
|
|
||
| import sqlalchemy as sa | ||
| from alembic import op | ||
|
|
||
| revision: str = "e5cd73f8d1bb" # pragma: allowlist secret | ||
| down_revision: Union[str, Sequence[str], None] = "08c7cf36ddbe" # pragma: allowlist secret | ||
| branch_labels: Union[str, Sequence[str], None] = None | ||
| depends_on: Union[str, Sequence[str], None] = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| op.add_column( | ||
| "supplier_facilities", | ||
| sa.Column("phone", sa.Text(), nullable=True), | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| # Destructive: deletes every stored facility phone. Back up | ||
| # supplier_facilities.phone first if those values must be kept. | ||
| op.drop_column("supplier_facilities", "phone") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,6 +89,7 @@ async def list_supplier_facilities( | |
| city=f.city, | ||
| state=f.state, | ||
| postal_code=f.postal_code, | ||
| phone=f.phone, | ||
| fda_registration_number=f.fda_registration_number, | ||
| roles=f.roles or [], | ||
| ) | ||
|
|
@@ -115,6 +116,7 @@ async def create_supplier_facility( | |
| city=request.city.strip(), | ||
| state=request.state.strip(), | ||
| postal_code=request.postal_code.strip(), | ||
| phone=(request.phone.strip() if request.phone else None), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Synchronize the phone when a facility is created. Creating a facility stores the phone but does not write it to Neo4j. A supplier can submit a CTE before setting FTL categories, and As per path instructions: “Flag any change that could drop, truncate, silently default, reorder, or corrupt Key Data Elements (KDEs).”
|
||
| fda_registration_number=(request.fda_registration_number.strip() if request.fda_registration_number else None), | ||
| roles=[role.strip() for role in request.roles if role.strip()], | ||
| ) | ||
|
|
@@ -129,6 +131,7 @@ async def create_supplier_facility( | |
| city=facility.city, | ||
| state=facility.state, | ||
| postal_code=facility.postal_code, | ||
| phone=facility.phone, | ||
| fda_registration_number=facility.fda_registration_number, | ||
| roles=facility.roles or [], | ||
| ) | ||
|
|
@@ -255,6 +258,7 @@ async def check_supplier_facility_removal( | |
| "city": facility.city, | ||
| "state": facility.state, | ||
| "postal_code": facility.postal_code, | ||
| "phone": facility.phone or "", | ||
| }, | ||
| ) | ||
|
|
||
|
|
@@ -390,6 +394,7 @@ async def set_facility_ftl_categories( | |
| city=facility.city, | ||
| state=facility.state, | ||
| postal_code=facility.postal_code, | ||
| phone=facility.phone, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enqueue the phone sync in the database transaction. This call does not pass
|
||
| fda_registration_number=facility.fda_registration_number, | ||
| roles=facility.roles or [], | ||
| categories=categories, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,6 +63,10 @@ class SupplierFacilityCreateRequest(BaseModel): | |
| city: str = Field(min_length=2) | ||
| state: str = Field(min_length=2) | ||
| postal_code: str = Field(min_length=3) | ||
| phone: str | None = Field( | ||
| default=None, | ||
| description="Facility business phone (21 CFR 1.1310 location description)", | ||
| ) | ||
|
Comment on lines
+66
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| fda_registration_number: str | None = None | ||
| roles: list[str] = Field(default_factory=list) | ||
|
|
||
|
|
@@ -74,6 +78,7 @@ class SupplierFacilityResponse(BaseModel): | |
| city: str | ||
| state: str | ||
| postal_code: str | ||
| phone: str | None = None | ||
| fda_registration_number: str | None | ||
| roles: list[str] | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -364,6 +364,7 @@ class Facility: | |
| city: Optional[str] = None | ||
| state: Optional[str] = None | ||
| country: Optional[str] = "US" | ||
| phone: Optional[str] = None # A-2190 / 21 CFR 1.1310 location description | ||
| facility_type: FacilityType = FacilityType.UNKNOWN | ||
| tenant_id: Optional[str] = None | ||
| # Merkle hash-chain integrity | ||
|
|
@@ -384,6 +385,37 @@ def identifier(self) -> str: | |
| """Return the primary identifier (GLN preferred, then FDA reg, then name).""" | ||
| return self.gln or self.fda_registration or self.name | ||
|
|
||
| @property | ||
| def is_location_description_complete(self) -> bool: | ||
| """Whether this facility satisfies 21 CFR 1.1310 location description. | ||
|
|
||
| Complete only with business name + phone + physical address | ||
| (street address, or city+state). A GLN is not a stand-in by itself: | ||
| it counts only when the location master row has already resolved to | ||
| those three fields on this Facility node. A bare / unresolved GLN | ||
| leaves the description incomplete (flag; ingest must not reject — | ||
| A-2170). A TLC source reference is never a stand-in on Facility | ||
| (it only substitutes for the TLC source location field on an event | ||
| at ingest). See Food Safety Lens answers for A-2190 / PR #3771. | ||
| """ | ||
| name_ok = bool((self.name or "").strip()) | ||
| phone_ok = bool((self.phone or "").strip()) | ||
| address_ok = bool( | ||
| (self.address or "").strip() | ||
| or ((self.city or "").strip() and (self.state or "").strip()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require the full location description. A facility with only Carry and check the missing fields, and update the city-state test in
|
||
| ) | ||
| return name_ok and phone_ok and address_ok | ||
|
|
||
| @property | ||
| def has_unresolved_gln(self) -> bool: | ||
| """True when a GLN is stored but name+phone+address are incomplete. | ||
|
|
||
| Flag for operators / export; does not reject the record (A-2170). | ||
| """ | ||
| if not (self.gln or "").strip(): | ||
| return False | ||
| return not self.is_location_description_complete | ||
|
Comment on lines
+409
to
+417
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| @property | ||
| def node_properties(self) -> Dict[str, Any]: | ||
| """Return properties for Neo4j node creation.""" | ||
|
|
@@ -400,6 +432,8 @@ def node_properties(self) -> Dict[str, Any]: | |
| props["state"] = self.state | ||
| if self.country: | ||
| props["country"] = self.country | ||
| if self.phone: | ||
| props["phone"] = self.phone | ||
| props["facility_type"] = ( | ||
| self.facility_type.value if isinstance(self.facility_type, FacilityType) else self.facility_type | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| -- A-2190 / 21 CFR 1.1310: optional business phone on supplier_facilities. | ||
| -- Mirrors alembic 20261031_e5cd73f8d1bb_v142_supplier_facility_phone.py. | ||
| -- Additive and nullable. Synthetic fixtures only until SEL-590 clears. | ||
|
|
||
| ALTER TABLE public.supplier_facilities | ||
| ADD COLUMN IF NOT EXISTS phone text; | ||
|
|
||
| COMMENT ON COLUMN public.supplier_facilities.phone IS | ||
| 'Facility business phone for 21 CFR 1.1310 location description (A-2190)'; |
Uh oh!
There was an error while loading. Please reload this page.