Consume governed recipients and add operational checks
This commit is contained in:
@@ -27,6 +27,7 @@ from govoplan_core.core.modules import (
|
||||
PermissionDefinition,
|
||||
RoleTemplate,
|
||||
)
|
||||
from govoplan_core.core.operations import OperationalCheckProviderRegistration
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.core.postbox import (
|
||||
@@ -254,7 +255,7 @@ manifest = ModuleManifest(
|
||||
),
|
||||
ModuleInterfaceRequirement(
|
||||
name="addresses.recipient_source",
|
||||
version_min="0.1.0",
|
||||
version_min="0.1.9",
|
||||
version_max_exclusive="0.2.0",
|
||||
optional=True,
|
||||
),
|
||||
@@ -750,6 +751,17 @@ manifest = ModuleManifest(
|
||||
fromlist=["retention_capability"],
|
||||
).retention_capability(context),
|
||||
},
|
||||
operational_check_providers=(
|
||||
OperationalCheckProviderRegistration(
|
||||
module_id="campaigns",
|
||||
check_id="campaign.generated_eml_storage",
|
||||
provider=lambda: __import__(
|
||||
"govoplan_campaign.backend.operational_checks",
|
||||
fromlist=["generated_eml_storage_check"],
|
||||
).generated_eml_storage_check(),
|
||||
cache_seconds=60,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
from govoplan_core.core.operations import OperationalCheck
|
||||
from govoplan_campaign.backend.persistence.campaigns import BUILD_OUTPUT_DIR
|
||||
|
||||
|
||||
def generated_eml_storage_check() -> OperationalCheck:
|
||||
"""Verify the current EML evidence path and report its node-local boundary."""
|
||||
|
||||
root = Path(BUILD_OUTPUT_DIR)
|
||||
probe = root / ".govoplan-health" / f"{secrets.token_hex(16)}.probe"
|
||||
payload = secrets.token_bytes(64)
|
||||
try:
|
||||
probe.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
|
||||
with probe.open("xb") as handle:
|
||||
handle.write(payload)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
if probe.read_bytes() != payload:
|
||||
raise OSError("generated EML persistence returned different bytes")
|
||||
except OSError as exc:
|
||||
return OperationalCheck(
|
||||
id="campaign.generated_eml_storage",
|
||||
label="Generated Campaign EML evidence",
|
||||
state="error",
|
||||
detail=(
|
||||
"The generated EML evidence path failed a bounded durable-write probe "
|
||||
f"({type(exc).__name__})."
|
||||
),
|
||||
readiness_critical=True,
|
||||
metrics={"backend": "node_local_filesystem"},
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
probe.unlink(missing_ok=True)
|
||||
probe.parent.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return OperationalCheck(
|
||||
id="campaign.generated_eml_storage",
|
||||
label="Generated Campaign EML evidence",
|
||||
state="warning",
|
||||
detail=(
|
||||
"Generated EML passed a local write/fsync/read probe, but remains node-local. "
|
||||
"Use one shared runtime volume and include it in coordinated backups until "
|
||||
"Campaign evidence is migrated to managed object storage."
|
||||
),
|
||||
metrics={"backend": "node_local_filesystem"},
|
||||
)
|
||||
|
||||
@@ -356,7 +356,15 @@ def validate_campaign_version(
|
||||
postbox_available=postbox_integration().available,
|
||||
)
|
||||
report_json = report.model_dump(mode="json")
|
||||
report_json.update({"ok": report.ok, "error_count": report.error_count, "warning_count": report.warning_count})
|
||||
report_json.update(
|
||||
{
|
||||
"ok": report.ok,
|
||||
"error_count": report.error_count,
|
||||
"warning_count": report.warning_count,
|
||||
"validated_at": datetime.now(UTC).isoformat(),
|
||||
"validated_by_user_id": user_id,
|
||||
}
|
||||
)
|
||||
version.validation_summary = report_json
|
||||
|
||||
# Replace version-level semantic issues from previous validations.
|
||||
@@ -642,6 +650,7 @@ def build_campaign_version(
|
||||
tenant_id: str,
|
||||
version_id: str,
|
||||
write_eml: bool = True,
|
||||
user_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
version, snapshot_path, config = load_version_config(session, version_id)
|
||||
campaign = session.get(Campaign, version.campaign_id)
|
||||
@@ -687,6 +696,7 @@ def build_campaign_version(
|
||||
entries_by_index=entries_by_index,
|
||||
)
|
||||
report_json = _campaign_build_report(result, files)
|
||||
report_json["built_by_user_id"] = user_id
|
||||
version.build_summary = report_json
|
||||
editor_state = copy.deepcopy(version.editor_state or {})
|
||||
editor_state.pop("review_send", None)
|
||||
|
||||
@@ -19,6 +19,7 @@ from govoplan_campaign.backend.schemas import (
|
||||
CampaignRecipientAddressSourcesResponse,
|
||||
CampaignRecipientAddressSourceSnapshotRequest,
|
||||
CampaignRecipientAddressSourceSnapshotResponse,
|
||||
CampaignRecipientSnapshotExcludedItem,
|
||||
CampaignRecipientSnapshotItem,
|
||||
RecipientImportMappingProfileListResponse,
|
||||
RecipientImportMappingProfilePayload,
|
||||
@@ -764,7 +765,11 @@ def snapshot_campaign_recipient_address_source(
|
||||
)
|
||||
try:
|
||||
snapshot = getattr(capability, "snapshot")(
|
||||
session, principal, source_id=payload.source_id
|
||||
session,
|
||||
principal,
|
||||
source_id=payload.source_id,
|
||||
purpose=payload.purpose,
|
||||
requested_channels=("email",),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
@@ -775,6 +780,15 @@ def snapshot_campaign_recipient_address_source(
|
||||
CampaignRecipientSnapshotItem.model_validate(_capability_payload(item))
|
||||
for item in snapshot_payload.get("recipients", [])
|
||||
]
|
||||
excluded = [
|
||||
CampaignRecipientSnapshotExcludedItem.model_validate(_capability_payload(item))
|
||||
for item in snapshot_payload.get("excluded", [])
|
||||
]
|
||||
provenance = (
|
||||
snapshot_payload.get("provenance")
|
||||
if isinstance(snapshot_payload.get("provenance"), dict)
|
||||
else {}
|
||||
)
|
||||
return CampaignRecipientAddressSourceSnapshotResponse(
|
||||
source_id=str(snapshot_payload.get("source_id") or ""),
|
||||
source_label=str(snapshot_payload.get("source_label") or ""),
|
||||
@@ -782,9 +796,16 @@ def snapshot_campaign_recipient_address_source(
|
||||
source_revision=str(snapshot_payload.get("source_revision") or ""),
|
||||
generated_at=str(snapshot_payload.get("generated_at") or ""),
|
||||
recipients=recipients,
|
||||
provenance=snapshot_payload.get("provenance")
|
||||
if isinstance(snapshot_payload.get("provenance"), dict)
|
||||
else {},
|
||||
excluded=excluded,
|
||||
included_count=len(recipients),
|
||||
excluded_count=len(excluded),
|
||||
purpose=str(snapshot_payload.get("purpose") or payload.purpose),
|
||||
provenance={
|
||||
**provenance,
|
||||
"campaign_id": campaign_id,
|
||||
"purpose": payload.purpose,
|
||||
"requested_channels": ["email"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -658,6 +658,7 @@ def build_version(
|
||||
tenant_id=principal.tenant_id,
|
||||
version_id=version_id,
|
||||
write_eml=payload.write_eml if payload else True,
|
||||
user_id=principal.user.id,
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
|
||||
@@ -378,6 +378,7 @@ class CampaignRecipientAddressSourceSnapshotRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
source_id: str = Field(min_length=1)
|
||||
purpose: str = Field(default="campaign_delivery", min_length=1, max_length=120)
|
||||
|
||||
|
||||
class CampaignRecipientSnapshotItem(BaseModel):
|
||||
@@ -389,6 +390,18 @@ class CampaignRecipientSnapshotItem(BaseModel):
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class CampaignRecipientSnapshotExcludedItem(BaseModel):
|
||||
contact_id: str
|
||||
display_name: str
|
||||
channel: str
|
||||
target: str
|
||||
contact_point_id: str | None = None
|
||||
status: str
|
||||
reason_code: str | None = None
|
||||
explanation: str | None = None
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class CampaignRecipientAddressSourceSnapshotResponse(BaseModel):
|
||||
source_id: str
|
||||
source_label: str
|
||||
@@ -396,6 +409,10 @@ class CampaignRecipientAddressSourceSnapshotResponse(BaseModel):
|
||||
source_revision: str
|
||||
generated_at: str
|
||||
recipients: list[CampaignRecipientSnapshotItem] = Field(default_factory=list)
|
||||
excluded: list[CampaignRecipientSnapshotExcludedItem] = Field(default_factory=list)
|
||||
included_count: int = 0
|
||||
excluded_count: int = 0
|
||||
purpose: str = "campaign_delivery"
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user