Add governed contact channel facts

This commit is contained in:
2026-07-31 22:48:07 +02:00
parent 90a507d9a4
commit 41ccd4c807
11 changed files with 1364 additions and 14 deletions
+102
View File
@@ -19,6 +19,7 @@ from govoplan_addresses.backend.db.models import (
AddressSyncSource,
AddressSyncTombstone,
Contact,
ContactChannelRule,
ContactPostalAddress,
)
from govoplan_addresses.backend.capabilities import AddressesContactWriterCapability
@@ -62,6 +63,9 @@ from govoplan_addresses.backend.schemas import (
AddressSyncTombstoneListResponse,
AddressSyncTombstoneResponse,
ContactCreateRequest,
ContactChannelRuleCreateRequest,
ContactChannelRuleListResponse,
ContactChannelRuleResponse,
ContactListResponse,
ContactResponse,
ContactUpdateRequest,
@@ -79,6 +83,7 @@ from govoplan_addresses.backend.service import (
create_address_list_entry,
create_carddav_sync_source,
create_contact,
create_contact_channel_rule,
create_sync_source,
count_contacts,
delete_address_book,
@@ -89,12 +94,14 @@ from govoplan_addresses.backend.service import (
discover_carddav_address_books,
export_address_book_vcard,
export_contact_vcard,
end_contact_channel_rule,
finish_sync_attempt,
import_vcards,
list_address_list_entries,
list_address_lists,
list_address_books,
list_contacts,
list_contact_channel_rules,
list_sync_conflicts,
list_sync_diagnostics,
list_sync_sources,
@@ -156,6 +163,10 @@ def _contact_response(contact: Contact) -> ContactResponse:
return ContactResponse.model_validate(contact)
def _channel_rule_response(rule: ContactChannelRule) -> ContactChannelRuleResponse:
return ContactChannelRuleResponse.model_validate(rule)
def _address_list_response(address_list: AddressList, *, entry_count: int = 0) -> AddressListResponse:
return AddressListResponse.model_validate(
{
@@ -1081,6 +1092,97 @@ def api_update_contact(
raise _error(exc) from exc
@router.get("/contacts/{contact_id}/channel-rules", response_model=ContactChannelRuleListResponse)
def api_list_contact_channel_rules(
contact_id: str,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "addresses:governance:read")
try:
return ContactChannelRuleListResponse(
rules=[
_channel_rule_response(rule)
for rule in list_contact_channel_rules(session, principal, contact_id)
]
)
except AddressBookError as exc:
raise _error(exc) from exc
@router.post(
"/contacts/{contact_id}/channel-rules",
response_model=ContactChannelRuleResponse,
status_code=status.HTTP_201_CREATED,
)
def api_create_contact_channel_rule(
contact_id: str,
payload: ContactChannelRuleCreateRequest,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "addresses:governance:write")
try:
rule = create_contact_channel_rule(session, principal, contact_id, payload)
_audit_address_sync(
session,
principal,
action="addresses.channel_rule_created",
object_type="address_contact_channel_rule",
object_id=rule.id,
details={
"contact_id": contact_id,
"channel": rule.channel,
"purpose": rule.purpose,
"contact_point_id": rule.contact_point_id,
"decision": rule.decision,
"effective_from": rule.effective_from.isoformat() if rule.effective_from else None,
"effective_until": rule.effective_until.isoformat() if rule.effective_until else None,
"evidence_ref": rule.evidence_ref,
},
)
session.commit()
session.refresh(rule)
return _channel_rule_response(rule)
except AddressBookError as exc:
session.rollback()
raise _error(exc) from exc
@router.delete(
"/contact-channel-rules/{rule_id}",
response_model=ContactChannelRuleResponse,
)
def api_end_contact_channel_rule(
rule_id: str,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "addresses:governance:write")
try:
rule = end_contact_channel_rule(session, principal, rule_id)
_audit_address_sync(
session,
principal,
action="addresses.channel_rule_ended",
object_type="address_contact_channel_rule",
object_id=rule.id,
details={
"contact_id": rule.contact_id,
"channel": rule.channel,
"purpose": rule.purpose,
"decision": rule.decision,
"effective_until": rule.effective_until.isoformat() if rule.effective_until else None,
},
)
session.commit()
session.refresh(rule)
return _channel_rule_response(rule)
except AddressBookError as exc:
session.rollback()
raise _error(exc) from exc
@router.delete("/contacts/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
def api_delete_contact(
contact_id: str,