Финальная стабильная реализация

This commit is contained in:
mi
2026-08-21 11:48:24 +03:00
parent 6e24278b10
commit d2415fcfeb
17 changed files with 662 additions and 57 deletions
@@ -15,6 +15,7 @@ from app.domain import (
choose_newest,
full_jitter_delay,
parse_crm_datetime,
safe_hash,
select_email,
validate_phone,
)
@@ -199,6 +200,14 @@ class WorkflowEngine:
alert_code: str | None = None
if same_user:
selected_id = str(same_user["ID"])
if len(contacts) > 1:
await self._alert(
workflow_id,
profile.user_id,
"duplicate_contacts",
[str(item["ID"]) for item in contacts],
selected_external_id=selected_id,
)
elif not contacts:
selected_id = await self._create_contact(workflow_id, profile)
else:
@@ -214,15 +223,27 @@ class WorkflowEngine:
assert selected is not None
all_ids = [item.b24_id for item in candidates]
if selected.crm_user_id and selected.crm_user_id != str(profile.user_id):
selected_id = await self._create_contact(workflow_id, profile)
alert_code = "contact_owned_by_other_user"
selected_id = await self._create_contact(workflow_id, profile)
await self._alert(
workflow_id,
profile.user_id,
alert_code,
[*all_ids, selected_id],
selected_external_id=selected_id,
)
else:
selected_id = selected.b24_id
await self._write_identity(workflow_id, selected_id, profile.user_id, active=True)
if len(candidates) > 1:
alert_code = "duplicate_contacts"
if alert_code:
await self._alert(workflow_id, profile.user_id, alert_code, all_ids)
await self._alert(
workflow_id,
profile.user_id,
alert_code,
all_ids,
selected_external_id=selected_id,
)
await self._write_identity(workflow_id, selected_id, profile.user_id, active=True)
await self._activate_mapping(workflow_id, profile.user_id, selected_id)
async def _create_contact(self, workflow_id: uuid.UUID, profile: Profile) -> str:
@@ -551,31 +572,144 @@ class WorkflowEngine:
)
async def _alert(
self, workflow_id: uuid.UUID, user_id: uuid.UUID, alert_type: str, candidates: list[str]
self,
workflow_id: uuid.UUID,
user_id: uuid.UUID,
alert_type: str,
candidates: list[str],
*,
selected_external_id: str | None = None,
) -> None:
fingerprint = f"{alert_type}:{user_id}"
fingerprint = safe_hash(f"{alert_type}:{user_id}")
assert fingerprint is not None
async with self.repository.transaction() as connection:
alert = (
await connection.execute(
text(
"""
INSERT INTO bitrix_sync.business_alerts
(id,fingerprint,alert_type,severity,app_user_id,
selected_external_id,candidate_external_ids,workflow_id,status,
occurrence_count,first_occurred_at,last_occurred_at,created_at,updated_at)
VALUES (gen_random_uuid(),:fingerprint,
:type,'warning',:user_id,:selected_external_id,:candidates,
:workflow_id,'open',1,now(),now(),now(),now())
ON CONFLICT (alert_type,fingerprint) WHERE status='open'
DO UPDATE SET
selected_external_id=coalesce(
excluded.selected_external_id,
business_alerts.selected_external_id
),
candidate_external_ids=excluded.candidate_external_ids,
workflow_id=excluded.workflow_id,
occurrence_count=business_alerts.occurrence_count+1,
last_occurred_at=now(),updated_at=now()
RETURNING id,alert_number,fingerprint,alert_type,severity,app_user_id,
selected_external_id,candidate_external_ids,remote_item_id,
occurrence_count,first_occurred_at,last_occurred_at
"""
),
{
"fingerprint": fingerprint,
"type": alert_type,
"user_id": user_id,
"selected_external_id": selected_external_id,
"candidates": candidates,
"workflow_id": workflow_id,
},
)
).mappings().one()
alert_config = (
await connection.execute(
text(
"""
SELECT value_json FROM bitrix_sync.settings
WHERE key='business_alerts' AND active=true
AND validation_status='valid'
"""
)
)
).scalar_one_or_none()
if not isinstance(alert_config, dict):
raise RetryableWorkflow("business_alerts_not_configured")
entity_type_id = alert_config.get("entity_type_id")
stage_new = alert_config.get("stage_new")
if not entity_type_id or not stage_new:
raise RetryableWorkflow("business_alerts_not_configured")
title = (
f"Конфликт синхронизации #{alert['alert_number']}: {alert_type}; "
f"user={user_id}; contacts={','.join(candidates)}"
)
fields: dict[str, Any] = {
"title": title[:255],
"stageId": stage_new,
"contactIds": [int(candidate) for candidate in candidates if candidate.isdigit()],
"sourceId": self.settings.contact_source,
}
if category_id := alert_config.get("category_id"):
fields["categoryId"] = category_id
if responsible_id := alert_config.get("responsible_id"):
fields["assignedById"] = responsible_id
alert_values = {
"alert_number": str(alert["alert_number"]),
"fingerprint": alert["fingerprint"],
"alert_type": alert["alert_type"],
"severity": alert["severity"],
"app_user_id": str(alert["app_user_id"]),
"selected_external_id": alert["selected_external_id"] or "",
"occurrence_count": alert["occurrence_count"],
"first_occurred_at": alert["first_occurred_at"].isoformat(),
"last_occurred_at": alert["last_occurred_at"].isoformat(),
"workflow_id": str(workflow_id),
}
field_ids = alert_config.get("field_ids")
if isinstance(field_ids, dict):
for value_name, field_id in field_ids.items():
if value_name in alert_values and isinstance(field_id, str) and field_id:
fields[field_id] = alert_values[value_name]
remote_item_id = alert["remote_item_id"]
if remote_item_id:
await self._command(
workflow_id,
"alert_update",
"crm.item.update",
{
"entityTypeId": int(entity_type_id),
"id": remote_item_id,
"fields": fields,
},
mutating=True,
)
return
result = await self._command(
workflow_id,
"alert_add",
"crm.item.add",
{"entityTypeId": int(entity_type_id), "fields": fields},
mutating=True,
)
payload = result.result if isinstance(result.result, dict) else {}
item = payload.get("item", payload)
remote_item_id = item.get("id") if isinstance(item, dict) else None
if remote_item_id is None:
raise RetryableWorkflow("alert_item_id_missing")
async with self.repository.transaction() as connection:
await connection.execute(
text(
"""
INSERT INTO bitrix_sync.business_alerts
(id,fingerprint,alert_type,severity,app_user_id,candidate_external_ids,
workflow_id,status,occurrence_count,first_occurred_at,last_occurred_at,
created_at,updated_at)
VALUES (gen_random_uuid(),encode(digest(:fingerprint,'sha256'),'hex'),
:type,'warning',:user_id,:candidates,:workflow_id,'open',1,
now(),now(),now(),now())
ON CONFLICT (alert_type,fingerprint) WHERE status='open'
DO UPDATE SET occurrence_count=business_alerts.occurrence_count+1,
last_occurred_at=now(),updated_at=now()
UPDATE bitrix_sync.business_alerts
SET remote_item_id=:remote_item_id,remote_stage_id=:stage_id,updated_at=now()
WHERE id=:id
"""
),
{
"fingerprint": fingerprint,
"type": alert_type,
"user_id": user_id,
"candidates": candidates,
"workflow_id": workflow_id,
"id": alert["id"],
"remote_item_id": str(remote_item_id),
"stage_id": str(stage_new),
},
)