Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""Expand module-07 queue and stage canonical Bitrix mapping.
|
||||
|
||||
Revision ID: 0011_module07_contract
|
||||
Revises: 0010_contact_map_dedup
|
||||
Create Date: 2026-08-06
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0011_module07_contract"
|
||||
down_revision: str | None = "0010_contact_map_dedup"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Expand first. Existing rows remain readable throughout the migration.
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE han_app.sync_queue
|
||||
ADD COLUMN IF NOT EXISTS locked_by varchar(128),
|
||||
ADD COLUMN IF NOT EXISTS locked_until timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS lease_token uuid,
|
||||
ADD COLUMN IF NOT EXISTS last_error_code varchar(64),
|
||||
ADD COLUMN IF NOT EXISTS last_error_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS completed_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS cancel_reason varchar(255);
|
||||
|
||||
UPDATE han_app.sync_queue
|
||||
SET status = CASE status
|
||||
WHEN 'processing' THEN 'pending'
|
||||
WHEN 'failed' THEN 'retry_wait'
|
||||
ELSE status
|
||||
END
|
||||
WHERE status IN ('processing', 'failed');
|
||||
|
||||
ALTER TABLE han_app.sync_queue
|
||||
DROP CONSTRAINT IF EXISTS sync_queue_status_check;
|
||||
ALTER TABLE han_app.sync_queue
|
||||
ADD CONSTRAINT sync_queue_status_check CHECK (
|
||||
status IN ('pending','leased','processed','retry_wait','dead_letter','cancelled')
|
||||
) NOT VALID;
|
||||
ALTER TABLE han_app.sync_queue
|
||||
VALIDATE CONSTRAINT sync_queue_status_check;
|
||||
|
||||
ALTER TABLE han_app.sync_queue
|
||||
DROP CONSTRAINT IF EXISTS sync_queue_dedup_key_key;
|
||||
DROP INDEX IF EXISTS han_app.ix_sync_queue_status_next;
|
||||
CREATE INDEX IF NOT EXISTS ix_sync_queue_claim
|
||||
ON han_app.sync_queue(status, next_attempt_at, created_at);
|
||||
CREATE INDEX IF NOT EXISTS ix_sync_queue_expired_lease
|
||||
ON han_app.sync_queue(locked_until) WHERE status = 'leased';
|
||||
CREATE INDEX IF NOT EXISTS ix_sync_queue_entity_history
|
||||
ON han_app.sync_queue(entity_type, entity_id, created_at DESC);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_sync_queue_active_dedup
|
||||
ON han_app.sync_queue(dedup_key)
|
||||
WHERE status IN ('pending','leased','retry_wait');
|
||||
"""
|
||||
)
|
||||
|
||||
# The bitrix-sync migration owns the canonical schema. If that migration
|
||||
# already ran, copy and verify legacy rows here; otherwise its follow-up
|
||||
# migration performs the same copy. The legacy table remains until the
|
||||
# readers have switched and the contract migration is explicitly approved.
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('bitrix_sync.entity_external_mapping') IS NOT NULL THEN
|
||||
INSERT INTO bitrix_sync.entity_external_mapping (
|
||||
id, entity_type, entity_id, external_system, external_entity_type,
|
||||
external_id, status, opened_at, created_at, updated_at
|
||||
)
|
||||
SELECT id, entity_type, entity_id, 'bitrix24', 'contact',
|
||||
external_id, 'active', created_at, created_at, created_at
|
||||
FROM han_app.entity_external_mapping
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM han_app.entity_external_mapping legacy
|
||||
LEFT JOIN bitrix_sync.entity_external_mapping canonical
|
||||
ON canonical.id = legacy.id
|
||||
AND canonical.entity_type = legacy.entity_type
|
||||
AND canonical.entity_id = legacy.entity_id
|
||||
AND canonical.external_id = legacy.external_id
|
||||
WHERE canonical.id IS NULL
|
||||
) THEN
|
||||
RAISE EXCEPTION 'canonical mapping verification failed';
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION han_app.enqueue_contact_sync()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = han_app, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
v_user_id uuid;
|
||||
v_task_type varchar(64);
|
||||
v_reason varchar(64);
|
||||
v_dedup varchar(255);
|
||||
v_source_updated_at timestamptz;
|
||||
BEGIN
|
||||
IF current_setting('han.sync_suppress', true) = 'true' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF TG_TABLE_NAME = 'user_identities' THEN
|
||||
v_user_id := NEW.id;
|
||||
v_source_updated_at := NEW.updated_at;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
IF NEW.record_status <> 'A' THEN RETURN NEW; END IF;
|
||||
v_task_type := 'contact.map_or_create';
|
||||
v_reason := 'identity_created';
|
||||
ELSIF OLD.record_status = 'A' AND NEW.record_status <> 'A' THEN
|
||||
v_task_type := 'contact.deactivate';
|
||||
v_reason := 'identity_deactivated';
|
||||
ELSIF OLD.record_status <> 'A' AND NEW.record_status = 'A' THEN
|
||||
v_task_type := 'contact.map_or_create';
|
||||
v_reason := 'identity_reactivated';
|
||||
ELSIF NEW.record_status = 'A'
|
||||
AND NEW.phone_number IS DISTINCT FROM OLD.phone_number THEN
|
||||
v_task_type := 'contact.update';
|
||||
v_reason := 'identity_phone_changed';
|
||||
ELSE
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
ELSE
|
||||
v_user_id := NEW.user_id;
|
||||
v_source_updated_at := NEW.updated_at;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
IF NEW.record_status <> 'A' THEN RETURN NEW; END IF;
|
||||
v_task_type := 'contact.map_or_create';
|
||||
v_reason := 'profile_created';
|
||||
ELSIF OLD.record_status = 'A' AND NEW.record_status <> 'A' THEN
|
||||
v_task_type := 'contact.deactivate';
|
||||
v_reason := 'profile_deactivated';
|
||||
ELSIF OLD.record_status <> 'A' AND NEW.record_status = 'A' THEN
|
||||
v_task_type := 'contact.map_or_create';
|
||||
v_reason := 'profile_reactivated';
|
||||
ELSE
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
v_dedup := v_task_type || ':' || v_user_id::text;
|
||||
INSERT INTO han_app.sync_queue (
|
||||
id, task_type, entity_type, entity_id, dedup_key, payload_json,
|
||||
status, attempt_count, next_attempt_at, created_at, updated_at
|
||||
) VALUES (
|
||||
gen_random_uuid(), v_task_type, 'contact', v_user_id, v_dedup,
|
||||
jsonb_build_object(
|
||||
'schema_version', 1,
|
||||
'user_id', v_user_id,
|
||||
'reason', v_reason,
|
||||
'source_updated_at', v_source_updated_at
|
||||
),
|
||||
'pending', 0, now(), now(), now()
|
||||
)
|
||||
ON CONFLICT (dedup_key)
|
||||
WHERE status IN ('pending','leased','retry_wait')
|
||||
DO UPDATE SET
|
||||
payload_json = EXCLUDED.payload_json,
|
||||
updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_profile_contact_sync ON han_app.client_profiles;
|
||||
CREATE TRIGGER trg_profile_contact_sync
|
||||
AFTER INSERT OR UPDATE OF record_status
|
||||
ON han_app.client_profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION han_app.enqueue_contact_sync();
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise RuntimeError("Module-07 staged contract migration is forward-only")
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Persist Message Safety v2 evidence and recovery locations.
|
||||
|
||||
Revision ID: 0012_safety_v2_checkpoint
|
||||
Revises: 0011_module07_contract
|
||||
Create Date: 2026-08-06
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0012_safety_v2_checkpoint"
|
||||
down_revision: str | None = "0011_module07_contract"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE han_app.messages
|
||||
ADD COLUMN IF NOT EXISTS safety_processing_mode varchar(16),
|
||||
ADD COLUMN IF NOT EXISTS safety_config_version bigint,
|
||||
ADD COLUMN IF NOT EXISTS safety_rules_version varchar(128);
|
||||
|
||||
ALTER TABLE han_app.message_attachments
|
||||
ADD COLUMN IF NOT EXISTS quarantine_version_id varchar(1024),
|
||||
ADD COLUMN IF NOT EXISTS quarantine_etag varchar(1024);
|
||||
ALTER TABLE han_app.message_attachments
|
||||
DROP CONSTRAINT IF EXISTS message_attachments_scan_status_check;
|
||||
ALTER TABLE han_app.message_attachments
|
||||
ADD CONSTRAINT message_attachments_scan_status_check
|
||||
CHECK (scan_status IN ('pending','clean','bypassed','infected','failed')) NOT VALID;
|
||||
ALTER TABLE han_app.message_attachments
|
||||
VALIDATE CONSTRAINT message_attachments_scan_status_check;
|
||||
|
||||
ALTER TABLE han_app.safety_tasks
|
||||
ADD COLUMN IF NOT EXISTS poll_location varchar(1024),
|
||||
ADD COLUMN IF NOT EXISTS processing_mode varchar(16),
|
||||
ADD COLUMN IF NOT EXISTS config_version bigint,
|
||||
ADD COLUMN IF NOT EXISTS rules_version varchar(128),
|
||||
ADD COLUMN IF NOT EXISTS expires_at timestamptz;
|
||||
UPDATE han_app.safety_tasks
|
||||
SET poll_location = '/internal/safety/v2/messages/tasks/' || task_id,
|
||||
expires_at = deadline_at
|
||||
WHERE poll_location IS NULL OR expires_at IS NULL;
|
||||
ALTER TABLE han_app.safety_tasks
|
||||
ALTER COLUMN poll_location SET NOT NULL,
|
||||
ALTER COLUMN expires_at SET NOT NULL;
|
||||
"""
|
||||
)
|
||||
|
||||
# Notification uploads use the same safety contract.
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE han_app.client_upload_drafts
|
||||
ADD COLUMN IF NOT EXISTS quarantine_version_id varchar(1024),
|
||||
ADD COLUMN IF NOT EXISTS quarantine_etag varchar(1024),
|
||||
ADD COLUMN IF NOT EXISTS safety_processing_mode varchar(16),
|
||||
ADD COLUMN IF NOT EXISTS safety_config_version bigint,
|
||||
ADD COLUMN IF NOT EXISTS safety_rules_version varchar(128);
|
||||
ALTER TABLE han_app.client_upload_drafts
|
||||
DROP CONSTRAINT IF EXISTS client_upload_drafts_scan_status_check;
|
||||
ALTER TABLE han_app.client_upload_drafts
|
||||
ADD CONSTRAINT client_upload_drafts_scan_status_check
|
||||
CHECK (scan_status IN ('pending','clean','bypassed','infected','failed')) NOT VALID;
|
||||
ALTER TABLE han_app.client_upload_drafts
|
||||
VALIDATE CONSTRAINT client_upload_drafts_scan_status_check;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise RuntimeError("Safety v2 checkpoint migration is forward-only")
|
||||
Reference in New Issue
Block a user