feat(postbox): schedule lifecycle notification reconciliation
This commit is contained in:
@@ -1188,7 +1188,7 @@ def dispatch_postbox_routes(
|
|||||||
tenant_id: str | None = None,
|
tenant_id: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
):
|
):
|
||||||
"""Deliver due Postbox vacancy escalations from durable route rows."""
|
"""Deliver due routes and reconcile assignment-derived notification facts."""
|
||||||
|
|
||||||
from govoplan_core.db.session import get_database
|
from govoplan_core.db.session import get_database
|
||||||
|
|
||||||
@@ -1202,21 +1202,40 @@ def dispatch_postbox_routes(
|
|||||||
"cancelled": 0,
|
"cancelled": 0,
|
||||||
"failed": 0,
|
"failed": 0,
|
||||||
"route_ids": [],
|
"route_ids": [],
|
||||||
|
"lifecycle_scanned": 0,
|
||||||
|
"lifecycle_changed": 0,
|
||||||
|
"lifecycle_events": 0,
|
||||||
|
"lifecycle_notifications": 0,
|
||||||
|
"lifecycle_notification_failures": 0,
|
||||||
}
|
}
|
||||||
if not registry.has_capability(CAPABILITY_POSTBOX_ROUTING):
|
if not registry.has_capability(CAPABILITY_POSTBOX_ROUTING):
|
||||||
return defaults
|
return defaults
|
||||||
|
provider = _postbox_routing_provider(registry)
|
||||||
|
|
||||||
|
def dispatch_tenant(effective_tenant_id: str) -> Mapping[str, object]:
|
||||||
|
route_result = provider.dispatch_due_routes( # type: ignore[union-attr]
|
||||||
|
session,
|
||||||
|
tenant_id=effective_tenant_id,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
lifecycle_result = provider.reconcile_notification_lifecycle( # type: ignore[union-attr]
|
||||||
|
session,
|
||||||
|
tenant_id=effective_tenant_id,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
**route_result,
|
||||||
|
**{
|
||||||
|
f"lifecycle_{key}": value for key, value in lifecycle_result.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
result = _run_tenant_worker_batches(
|
result = _run_tenant_worker_batches(
|
||||||
registry,
|
registry,
|
||||||
session,
|
session,
|
||||||
capability_name=CAPABILITY_POSTBOX_ROUTING,
|
capability_name=CAPABILITY_POSTBOX_ROUTING,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
operation=lambda effective_tenant_id: _postbox_routing_provider(
|
operation=dispatch_tenant,
|
||||||
registry
|
|
||||||
).dispatch_due_routes( # type: ignore[union-attr]
|
|
||||||
session,
|
|
||||||
tenant_id=effective_tenant_id,
|
|
||||||
limit=limit,
|
|
||||||
),
|
|
||||||
defaults=defaults,
|
defaults=defaults,
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ class PostboxDeliveryRequest:
|
|||||||
body_text: str | None = None
|
body_text: str | None = None
|
||||||
sender_label: str | None = None
|
sender_label: str | None = None
|
||||||
classification: str = "internal"
|
classification: str = "internal"
|
||||||
|
action_required: bool = False
|
||||||
participants: tuple[PostboxParticipantRef, ...] = ()
|
participants: tuple[PostboxParticipantRef, ...] = ()
|
||||||
attachments: tuple[PostboxAttachmentRef, ...] = ()
|
attachments: tuple[PostboxAttachmentRef, ...] = ()
|
||||||
expires_at: datetime | None = None
|
expires_at: datetime | None = None
|
||||||
@@ -511,6 +512,17 @@ class PostboxRoutingProvider(Protocol):
|
|||||||
) -> Mapping[str, object]:
|
) -> Mapping[str, object]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
def reconcile_notification_lifecycle(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> Mapping[str, object]:
|
||||||
|
"""Reconcile assignment-derived Postbox notification facts."""
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class PostboxPortalProjectionProvider(Protocol):
|
class PostboxPortalProjectionProvider(Protocol):
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ class _RoutingProvider:
|
|||||||
del session, tenant_id, limit
|
del session, tenant_id, limit
|
||||||
return {"selected": 0}
|
return {"selected": 0}
|
||||||
|
|
||||||
|
def reconcile_notification_lifecycle(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
*,
|
||||||
|
tenant_id=None,
|
||||||
|
limit=50,
|
||||||
|
):
|
||||||
|
del session, tenant_id, limit
|
||||||
|
return {"scanned": 0}
|
||||||
|
|
||||||
|
|
||||||
class PostboxRoutingWorkerTests(unittest.TestCase):
|
class PostboxRoutingWorkerTests(unittest.TestCase):
|
||||||
def test_contract_is_runtime_checkable_and_resolved(self) -> None:
|
def test_contract_is_runtime_checkable_and_resolved(self) -> None:
|
||||||
@@ -59,6 +69,13 @@ class PostboxRoutingWorkerTests(unittest.TestCase):
|
|||||||
"selected": 1,
|
"selected": 1,
|
||||||
"delivered": 1,
|
"delivered": 1,
|
||||||
}
|
}
|
||||||
|
provider.reconcile_notification_lifecycle.return_value = {
|
||||||
|
"scanned": 2,
|
||||||
|
"changed": 1,
|
||||||
|
"events": 2,
|
||||||
|
"notifications": 1,
|
||||||
|
"notification_failures": 0,
|
||||||
|
}
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
@@ -81,8 +98,15 @@ class PostboxRoutingWorkerTests(unittest.TestCase):
|
|||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
limit=25,
|
limit=25,
|
||||||
)
|
)
|
||||||
|
provider.reconcile_notification_lifecycle.assert_called_once_with(
|
||||||
|
session,
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
limit=25,
|
||||||
|
)
|
||||||
session.commit.assert_called_once_with()
|
session.commit.assert_called_once_with()
|
||||||
self.assertEqual(1, result["delivered"])
|
self.assertEqual(1, result["delivered"])
|
||||||
|
self.assertEqual(2, result["lifecycle_scanned"])
|
||||||
|
self.assertEqual(1, result["lifecycle_notifications"])
|
||||||
|
|
||||||
def test_route_and_periodic_recovery_are_registered(self) -> None:
|
def test_route_and_periodic_recovery_are_registered(self) -> None:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
Reference in New Issue
Block a user