Разработана первая версия приложений
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""Initial han_app schema, seed and CRM sync triggers.
|
||||
|
||||
Revision ID: 0001_initial
|
||||
Revises:
|
||||
Create Date: 2026-07-10
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
from app.db import Base
|
||||
|
||||
revision: str = "0001_initial"
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
SEED = {
|
||||
"auth.phone.enabled": ("true", "boolean", True),
|
||||
"auth.password.enabled": ("false", "boolean", True),
|
||||
"otp.phone.max_send_attempts_per_24h": ("3", "integer", False),
|
||||
"otp.phone.min_seconds_between_attempts": ("30", "integer", False),
|
||||
"operator.call.phone": ("+74999591007", "string", True),
|
||||
"consent.personal_data.required": ("true", "boolean", True),
|
||||
"consent.personal_data.document_url": (
|
||||
"https://www.han0107.ru/privacy/persdata-agree-mobile",
|
||||
"string",
|
||||
True,
|
||||
),
|
||||
"consent.personal_data.version": ("2026-06-10", "string", True),
|
||||
"consent.user_agreement.required": ("true", "boolean", True),
|
||||
"consent.user_agreement.document_url": (
|
||||
"https://www.han0107.ru/user-agreement",
|
||||
"string",
|
||||
True,
|
||||
),
|
||||
"consent.user_agreement.version": ("2026-06-10", "string", True),
|
||||
"consent.marketing.required": ("false", "boolean", True),
|
||||
"consent.marketing.version": ("2026-06-10", "string", True),
|
||||
"chat.attachments.allowed_extensions": (
|
||||
"jpg,jpeg,png,webp,heic,heif,pdf",
|
||||
"string_list",
|
||||
True,
|
||||
),
|
||||
"chat.attachments.allowed_mime_types": (
|
||||
"image/jpeg,image/png,image/webp,image/heic,image/heif,application/pdf",
|
||||
"string_list",
|
||||
True,
|
||||
),
|
||||
"chat.attachments.disallowed_extensions": ("svg,doc,docx,xls,xlsx,csv", "string_list", False),
|
||||
"chat.attachments.max_size_mb": ("5", "integer", True),
|
||||
"chat.attachments.storage": ("selectel_s3", "string", False),
|
||||
"chat.attachments.upload_mode": ("presigned_put", "string", False),
|
||||
"chat.attachments.safety_scan_required": ("true", "boolean", False),
|
||||
"chat.attachments.presigned_upload_ttl_seconds": ("600", "integer", False),
|
||||
"rate_limit.message_send.per_user": ("30/minute", "string", False),
|
||||
"rate_limit.message_send.per_dialog": ("20/minute", "string", False),
|
||||
"rate_limit.download_url.per_user": ("60/hour", "string", False),
|
||||
"rate_limit.public_endpoints.per_ip": ("60/minute", "string", False),
|
||||
"rate_limit.login.per_ip": ("10/minute", "string", False),
|
||||
"ux.session.idle_timeout_minutes": ("30", "integer", True),
|
||||
"security.cors.allowed_origins": ("https://tohin.ru", "string_list", False),
|
||||
"security.public_cache.max_age_seconds": ("3600", "integer", False),
|
||||
}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
|
||||
op.execute("CREATE SCHEMA IF NOT EXISTS han_app")
|
||||
Base.metadata.create_all(bind=bind, checkfirst=True)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_dialog_one_active_per_user
|
||||
ON han_app.dialogs(user_id)
|
||||
WHERE record_status='A'
|
||||
AND status IN ('open','waiting_for_company','waiting_for_client');
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_profiles_active_bitrix_contact
|
||||
ON han_app.client_profiles(bitrix_contact_id)
|
||||
WHERE bitrix_contact_id IS NOT NULL AND record_status='A';
|
||||
|
||||
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_entity_id uuid;
|
||||
v_task_type text;
|
||||
v_dedup text;
|
||||
BEGIN
|
||||
IF current_setting('han.sync_suppress', true) = 'true' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
IF TG_TABLE_NAME = 'user_identities' THEN
|
||||
v_entity_id := NEW.id;
|
||||
v_task_type := CASE WHEN TG_OP = 'INSERT'
|
||||
THEN 'contact.map_or_create' ELSE 'contact.update' END;
|
||||
ELSE
|
||||
v_entity_id := NEW.user_id;
|
||||
v_task_type := CASE WHEN TG_OP = 'INSERT'
|
||||
THEN 'contact.map_or_create' ELSE 'contact.update' END;
|
||||
END IF;
|
||||
v_dedup := v_task_type || ':' || v_entity_id::text || ':' ||
|
||||
encode(digest(row_to_json(NEW)::text, 'sha256'), 'hex');
|
||||
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_entity_id, v_dedup,
|
||||
jsonb_build_object('entity_id', v_entity_id), 'pending', 0,
|
||||
now(), now(), now())
|
||||
ON CONFLICT (dedup_key) DO NOTHING;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_identity_contact_sync ON han_app.user_identities;
|
||||
CREATE TRIGGER trg_identity_contact_sync
|
||||
AFTER INSERT OR UPDATE OF phone_number, record_status
|
||||
ON han_app.user_identities
|
||||
FOR EACH ROW EXECUTE FUNCTION han_app.enqueue_contact_sync();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_profile_contact_sync ON han_app.client_profiles;
|
||||
CREATE TRIGGER trg_profile_contact_sync
|
||||
AFTER INSERT OR UPDATE OF full_name, citizenship, russian_phone,
|
||||
foreign_phone, email, record_status
|
||||
ON han_app.client_profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION han_app.enqueue_contact_sync();
|
||||
"""
|
||||
)
|
||||
for key, (value, value_type, public) in SEED.items():
|
||||
bind.exec_driver_sql(
|
||||
"""
|
||||
INSERT INTO han_app.app_settings
|
||||
(setting_key, setting_value, value_type, is_public, record_status, updated_at)
|
||||
VALUES (%s, %s, %s, %s, 'A', now())
|
||||
ON CONFLICT (setting_key) DO UPDATE SET
|
||||
setting_value = EXCLUDED.setting_value,
|
||||
value_type = EXCLUDED.value_type,
|
||||
is_public = EXCLUDED.is_public,
|
||||
record_status = 'A',
|
||||
updated_at = now()
|
||||
""",
|
||||
(key, value, value_type, public),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise RuntimeError("Initial data migration is forward-only")
|
||||
Reference in New Issue
Block a user