Add governed contact point snapshots
This commit is contained in:
@@ -8,6 +8,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_from_principal
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.core.contact_points import (
|
||||
ContactPointResolutionRequest,
|
||||
ContactPointSourceRequest,
|
||||
)
|
||||
from govoplan_core.core.distribution_lists import DistributionSourceReference
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_addresses.backend.carddav import AddressCardDAVError
|
||||
from govoplan_addresses.backend.db.models import (
|
||||
@@ -22,7 +27,10 @@ from govoplan_addresses.backend.db.models import (
|
||||
ContactChannelRule,
|
||||
ContactPostalAddress,
|
||||
)
|
||||
from govoplan_addresses.backend.capabilities import AddressesContactWriterCapability
|
||||
from govoplan_addresses.backend.capabilities import (
|
||||
AddressesContactPointResolutionCapability,
|
||||
AddressesContactWriterCapability,
|
||||
)
|
||||
from govoplan_addresses.backend.schemas import (
|
||||
AddressBookCreateRequest,
|
||||
AddressBookListResponse,
|
||||
@@ -66,6 +74,11 @@ from govoplan_addresses.backend.schemas import (
|
||||
ContactChannelRuleCreateRequest,
|
||||
ContactChannelRuleListResponse,
|
||||
ContactChannelRuleResponse,
|
||||
ContactPointResolveRequest,
|
||||
ContactPointResolutionResponse,
|
||||
ContactPointSnapshotResponse,
|
||||
ContactPointSourcePreviewResponse,
|
||||
ContactPointSourceRequestPayload,
|
||||
ContactListResponse,
|
||||
ContactResponse,
|
||||
ContactUpdateRequest,
|
||||
@@ -212,6 +225,43 @@ def _write_decision_response(decision) -> AddressBookWriteDecisionResponse:
|
||||
return AddressBookWriteDecisionResponse.model_validate(payload)
|
||||
|
||||
|
||||
def _contact_point_resolution_request(
|
||||
principal: ApiPrincipal,
|
||||
payload: ContactPointResolveRequest,
|
||||
) -> ContactPointResolutionRequest:
|
||||
return ContactPointResolutionRequest(
|
||||
tenant_id=principal.tenant_id,
|
||||
subject=DistributionSourceReference(**payload.subject.model_dump()),
|
||||
effective_at=payload.effective_at,
|
||||
purpose=payload.purpose,
|
||||
requested_channels=tuple(payload.requested_channels),
|
||||
address_purpose=payload.address_purpose,
|
||||
fallback_rule=payload.fallback_rule,
|
||||
locale=payload.locale,
|
||||
postal_format=payload.postal_format,
|
||||
context=payload.context,
|
||||
)
|
||||
|
||||
|
||||
def _contact_point_source_request(
|
||||
principal: ApiPrincipal,
|
||||
payload: ContactPointSourceRequestPayload,
|
||||
) -> ContactPointSourceRequest:
|
||||
return ContactPointSourceRequest(
|
||||
tenant_id=principal.tenant_id,
|
||||
source_id=payload.source_id,
|
||||
effective_at=payload.effective_at,
|
||||
purpose=payload.purpose,
|
||||
requested_channels=tuple(payload.requested_channels),
|
||||
address_purpose=payload.address_purpose,
|
||||
fallback_rule=payload.fallback_rule,
|
||||
locale=payload.locale,
|
||||
postal_format=payload.postal_format,
|
||||
max_items=payload.max_items,
|
||||
context=payload.context,
|
||||
)
|
||||
|
||||
|
||||
def _sync_source_response(sync_source: AddressSyncSource) -> AddressSyncSourceResponse:
|
||||
return AddressSyncSourceResponse.model_validate(
|
||||
{
|
||||
@@ -493,6 +543,124 @@ def api_lookup_addresses(
|
||||
return AddressLookupResponse(contacts=[_contact_response(contact) for contact in contacts])
|
||||
|
||||
|
||||
@router.post("/contact-points/resolve", response_model=ContactPointResolutionResponse)
|
||||
def api_resolve_contact_points(
|
||||
payload: ContactPointResolveRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
try:
|
||||
result = AddressesContactPointResolutionCapability().resolve_contact_points(
|
||||
session,
|
||||
principal,
|
||||
request=_contact_point_resolution_request(principal, payload),
|
||||
)
|
||||
return ContactPointResolutionResponse.model_validate(asdict(result))
|
||||
except (AddressBookError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/contact-point-sources/preview",
|
||||
response_model=ContactPointSourcePreviewResponse,
|
||||
)
|
||||
def api_preview_contact_point_source(
|
||||
payload: ContactPointSourceRequestPayload,
|
||||
offset: int = Query(default=0, ge=0),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
try:
|
||||
result = AddressesContactPointResolutionCapability().preview_source(
|
||||
session,
|
||||
principal,
|
||||
request=_contact_point_source_request(principal, payload),
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
return ContactPointSourcePreviewResponse.model_validate(asdict(result))
|
||||
except (AddressBookError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/contact-point-snapshots",
|
||||
response_model=ContactPointSnapshotResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_freeze_contact_point_source(
|
||||
payload: ContactPointSourceRequestPayload,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
try:
|
||||
snapshot = AddressesContactPointResolutionCapability().freeze_source(
|
||||
session,
|
||||
principal,
|
||||
request=_contact_point_source_request(principal, payload),
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.contact_point_snapshot_created",
|
||||
object_type="address_contact_point_snapshot",
|
||||
object_id=snapshot.id,
|
||||
details={
|
||||
"source_id": snapshot.request.source_id,
|
||||
"source_revision": snapshot.source_revision,
|
||||
"snapshot_hash": snapshot.snapshot_hash,
|
||||
"recipient_count": snapshot.recipient_count,
|
||||
"excluded_count": snapshot.excluded_count,
|
||||
"purpose": snapshot.request.purpose,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return ContactPointSnapshotResponse.model_validate(asdict(snapshot))
|
||||
except (AddressBookError, ValueError) as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/contact-point-snapshots/{snapshot_id}",
|
||||
response_model=ContactPointSnapshotResponse,
|
||||
)
|
||||
def api_get_contact_point_snapshot(
|
||||
snapshot_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
snapshot = AddressesContactPointResolutionCapability().get_snapshot(
|
||||
session,
|
||||
principal,
|
||||
snapshot_id=snapshot_id,
|
||||
)
|
||||
if snapshot is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Contact-point snapshot not found.",
|
||||
)
|
||||
return ContactPointSnapshotResponse.model_validate(asdict(snapshot))
|
||||
|
||||
|
||||
@router.get("/address-lists", response_model=AddressListListResponse)
|
||||
def api_list_address_lists(
|
||||
address_book_id: str | None = Query(default=None),
|
||||
|
||||
Reference in New Issue
Block a user