Добавлены уведомления
This commit is contained in:
@@ -0,0 +1,651 @@
|
||||
"""Notification Center v1 schema, catalog and settings.
|
||||
|
||||
Revision ID: 0008_notifications_v1
|
||||
Revises: 0007_marketing_doc
|
||||
Create Date: 2026-07-27
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0008_notifications_v1"
|
||||
down_revision: str | None = "0007_marketing_doc"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
COMMON = """
|
||||
record_status varchar(1) NOT NULL DEFAULT 'A',
|
||||
status_changed_at timestamptz NULL,
|
||||
status_change_reason varchar(255) NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updater_user_id uuid NULL
|
||||
"""
|
||||
|
||||
|
||||
def _execute_batch(sql: str) -> None:
|
||||
"""Execute one statement at a time for the asyncpg prepared-statement dialect."""
|
||||
statement: list[str] = []
|
||||
in_dollar_quote = False
|
||||
index = 0
|
||||
while index < len(sql):
|
||||
if sql[index : index + 2] == "$$":
|
||||
in_dollar_quote = not in_dollar_quote
|
||||
statement.append("$$")
|
||||
index += 2
|
||||
continue
|
||||
character = sql[index]
|
||||
if character == ";" and not in_dollar_quote:
|
||||
value = "".join(statement).strip()
|
||||
if value:
|
||||
op.execute(value)
|
||||
statement.clear()
|
||||
else:
|
||||
statement.append(character)
|
||||
index += 1
|
||||
value = "".join(statement).strip()
|
||||
if value:
|
||||
op.execute(value)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
_execute_batch(
|
||||
f"""
|
||||
CREATE TABLE han_app.notification_cta_actions (
|
||||
id uuid PRIMARY KEY, code varchar(32) NOT NULL UNIQUE,
|
||||
description varchar(255) NOT NULL, requires_auth boolean NOT NULL,
|
||||
required_instance_fields varchar(64)[] NOT NULL DEFAULT '{{}}', {COMMON}
|
||||
);
|
||||
CREATE TABLE han_app.notification_buttons (
|
||||
id uuid PRIMARY KEY, code varchar(32) NOT NULL UNIQUE,
|
||||
label varchar(64) NOT NULL, sets_hidden boolean NOT NULL DEFAULT false,
|
||||
applies_hidden_ttl boolean NOT NULL DEFAULT false,
|
||||
close_reason varchar(32), submits_documents boolean NOT NULL DEFAULT false,
|
||||
{COMMON},
|
||||
CHECK (NOT applies_hidden_ttl OR sets_hidden),
|
||||
CHECK (NOT submits_documents OR close_reason IS NOT NULL)
|
||||
);
|
||||
CREATE TABLE han_app.notification_color_tokens (
|
||||
id uuid PRIMARY KEY, code varchar(32) NOT NULL UNIQUE,
|
||||
description varchar(255) NOT NULL, sort_order smallint NOT NULL, {COMMON}
|
||||
);
|
||||
CREATE TABLE han_app.notification_types (
|
||||
id uuid PRIMARY KEY, code varchar(32) NOT NULL UNIQUE,
|
||||
contour varchar(1) NOT NULL CHECK (contour IN ('G','P')),
|
||||
priority smallint NOT NULL, countable boolean NOT NULL,
|
||||
label varchar(64) NOT NULL,
|
||||
color_token varchar(32) NOT NULL REFERENCES han_app.notification_color_tokens(code)
|
||||
ON DELETE RESTRICT,
|
||||
icon_code varchar(32), cta_text varchar(64) NOT NULL,
|
||||
cta_action varchar(32) NOT NULL REFERENCES han_app.notification_cta_actions(code)
|
||||
ON DELETE RESTRICT,
|
||||
cta_sets_hidden boolean NOT NULL DEFAULT false,
|
||||
cta_close_reason varchar(32),
|
||||
button_primary_code varchar(32) REFERENCES han_app.notification_buttons(code)
|
||||
ON DELETE RESTRICT,
|
||||
button_secondary_code varchar(32) REFERENCES han_app.notification_buttons(code)
|
||||
ON DELETE RESTRICT,
|
||||
hidden_ttl_days smallint,
|
||||
documents_allowed boolean NOT NULL DEFAULT false,
|
||||
hide_on_document_download boolean NOT NULL DEFAULT false,
|
||||
required_detail_blocks varchar(64)[] NOT NULL DEFAULT '{{}}',
|
||||
{COMMON},
|
||||
CHECK (contour <> 'G' OR countable = false),
|
||||
CHECK (contour <> 'G' OR cta_action <> 'open_detail'),
|
||||
CHECK (cta_action <> 'open_detail' OR
|
||||
(cta_sets_hidden = false AND cta_close_reason IS NULL)),
|
||||
CHECK (cta_action = 'open_detail' OR
|
||||
(documents_allowed = false AND hide_on_document_download = false
|
||||
AND required_detail_blocks = '{{}}')),
|
||||
CHECK (NOT hide_on_document_download OR documents_allowed),
|
||||
CHECK (cta_action <> 'open_detail' OR button_primary_code IS NOT NULL),
|
||||
CHECK (cta_action = 'open_detail' OR
|
||||
(button_primary_code IS NULL AND button_secondary_code IS NULL)),
|
||||
CHECK (button_secondary_code IS NULL OR button_primary_code IS NOT NULL),
|
||||
CHECK (button_secondary_code IS NULL OR
|
||||
button_secondary_code <> button_primary_code)
|
||||
);
|
||||
CREATE TABLE han_app.notification_sources (
|
||||
id uuid PRIMARY KEY, code varchar(32) NOT NULL UNIQUE,
|
||||
description text, token_hash varchar(128) NOT NULL UNIQUE,
|
||||
token_rotated_at timestamptz, {COMMON}
|
||||
);
|
||||
CREATE TABLE han_app.notifications (
|
||||
id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES han_app.user_identities(id) ON DELETE RESTRICT,
|
||||
notification_type varchar(32) NOT NULL
|
||||
REFERENCES han_app.notification_types(code) ON DELETE RESTRICT,
|
||||
source varchar(32) NOT NULL
|
||||
REFERENCES han_app.notification_sources(code) ON DELETE RESTRICT,
|
||||
external_id varchar(128) NOT NULL,
|
||||
request_fingerprint varchar(64) NOT NULL,
|
||||
notification_datetime timestamptz NOT NULL,
|
||||
header varchar(255) NOT NULL, text varchar(1024),
|
||||
priority_override smallint, date_expired timestamptz,
|
||||
price numeric(12,2), old_price numeric(12,2), payment_url text,
|
||||
details jsonb, details_schema_version smallint NOT NULL DEFAULT 1,
|
||||
chat_message_text varchar(1024),
|
||||
lifecycle_status varchar(16) NOT NULL DEFAULT 'active'
|
||||
CHECK (lifecycle_status IN ('active','closed')),
|
||||
visibility varchar(16) NOT NULL DEFAULT 'visible'
|
||||
CHECK (visibility IN ('visible','hidden')),
|
||||
is_read boolean NOT NULL DEFAULT false,
|
||||
close_reason varchar(32) CHECK (close_reason IS NULL OR close_reason IN
|
||||
('user_done','docs_submitted','offer_accepted','paid','expired','cancelled')),
|
||||
closed_at timestamptz, {COMMON},
|
||||
CONSTRAINT uq_notifications_source_key UNIQUE (source, external_id),
|
||||
CHECK (old_price IS NULL OR price IS NOT NULL),
|
||||
CHECK (lifecycle_status <> 'closed' OR
|
||||
(close_reason IS NOT NULL AND closed_at IS NOT NULL))
|
||||
);
|
||||
CREATE INDEX ix_notifications_user_active
|
||||
ON han_app.notifications
|
||||
(user_id, lifecycle_status, visibility, notification_datetime DESC, id DESC)
|
||||
WHERE record_status='A';
|
||||
CREATE INDEX ix_notifications_expire ON han_app.notifications(date_expired)
|
||||
WHERE record_status='A' AND lifecycle_status='active'
|
||||
AND date_expired IS NOT NULL;
|
||||
CREATE TABLE han_app.guest_notifications (
|
||||
id uuid PRIMARY KEY,
|
||||
notification_type varchar(32) NOT NULL
|
||||
REFERENCES han_app.notification_types(code) ON DELETE RESTRICT,
|
||||
notification_datetime timestamptz NOT NULL,
|
||||
header varchar(255) NOT NULL, text varchar(1024),
|
||||
priority_override smallint, date_expired timestamptz,
|
||||
price numeric(12,2), old_price numeric(12,2),
|
||||
instruction_url text, chat_message_text varchar(1024),
|
||||
lifecycle_status varchar(16) NOT NULL DEFAULT 'active'
|
||||
CHECK (lifecycle_status IN ('active','closed')),
|
||||
closed_at timestamptz, {COMMON},
|
||||
CHECK (old_price IS NULL OR price IS NOT NULL),
|
||||
CHECK (instruction_url IS NULL OR instruction_url LIKE 'https://%')
|
||||
);
|
||||
CREATE INDEX ix_guest_notifications_active
|
||||
ON han_app.guest_notifications(lifecycle_status, notification_datetime DESC, id DESC)
|
||||
WHERE record_status='A';
|
||||
CREATE INDEX ix_guest_notifications_expire
|
||||
ON han_app.guest_notifications(date_expired)
|
||||
WHERE record_status='A' AND lifecycle_status='active'
|
||||
AND date_expired IS NOT NULL;
|
||||
CREATE TABLE han_app.notification_documents (
|
||||
id uuid PRIMARY KEY,
|
||||
notification_id uuid NOT NULL
|
||||
REFERENCES han_app.notifications(id) ON DELETE RESTRICT,
|
||||
document_id uuid NOT NULL REFERENCES han_app.documents(id) ON DELETE RESTRICT,
|
||||
sort_order smallint NOT NULL DEFAULT 0,
|
||||
download_url_issued_at timestamptz, {COMMON},
|
||||
UNIQUE (notification_id, document_id)
|
||||
);
|
||||
CREATE TABLE han_app.client_upload_drafts (
|
||||
id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES han_app.user_identities(id) ON DELETE RESTRICT,
|
||||
context_type varchar(32) NOT NULL CHECK (context_type IN ('notification')),
|
||||
context_id uuid NOT NULL,
|
||||
original_file_name varchar(255) NOT NULL,
|
||||
safe_file_name varchar(255) NOT NULL,
|
||||
mime_type varchar(128) NOT NULL,
|
||||
size_bytes bigint NOT NULL CHECK (size_bytes > 0),
|
||||
checksum_sha256 char(64),
|
||||
scan_status varchar(16) NOT NULL DEFAULT 'pending'
|
||||
CHECK (scan_status IN ('pending','clean','infected','failed')),
|
||||
storage_bucket varchar(255) NOT NULL,
|
||||
object_key varchar(1024) NOT NULL,
|
||||
quarantine_object_key varchar(1024),
|
||||
upload_expires_at timestamptz, completed_at timestamptz,
|
||||
state varchar(16) NOT NULL DEFAULT 'draft'
|
||||
CHECK (state IN ('draft','submitted','discarded')),
|
||||
submission_id uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX ix_client_upload_drafts_context
|
||||
ON han_app.client_upload_drafts(user_id, context_type, context_id)
|
||||
WHERE state='draft';
|
||||
CREATE INDEX ix_client_upload_drafts_scan
|
||||
ON han_app.client_upload_drafts(scan_status, updated_at);
|
||||
CREATE INDEX ix_client_upload_drafts_created
|
||||
ON han_app.client_upload_drafts(created_at);
|
||||
CREATE TABLE han_app.client_documents (
|
||||
id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES han_app.user_identities(id) ON DELETE RESTRICT,
|
||||
context_type varchar(32) NOT NULL, context_id uuid NOT NULL,
|
||||
submission_id uuid NOT NULL, source_draft_id uuid NOT NULL UNIQUE,
|
||||
original_file_name varchar(255) NOT NULL, safe_file_name varchar(255) NOT NULL,
|
||||
mime_type varchar(128) NOT NULL, size_bytes bigint NOT NULL,
|
||||
checksum_sha256 char(64) NOT NULL, storage_bucket varchar(255) NOT NULL,
|
||||
object_key varchar(1024) NOT NULL, submitted_at timestamptz NOT NULL,
|
||||
{COMMON}, UNIQUE (storage_bucket, object_key)
|
||||
);
|
||||
CREATE INDEX ix_client_documents_context
|
||||
ON han_app.client_documents(context_type, context_id);
|
||||
CREATE INDEX ix_client_documents_user_submitted
|
||||
ON han_app.client_documents(user_id, submitted_at DESC);
|
||||
"""
|
||||
)
|
||||
|
||||
_execute_batch(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION han_app.validate_guest_notification()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY INVOKER
|
||||
SET search_path = han_app, pg_temp AS $$
|
||||
DECLARE v_contour varchar(1); v_action varchar(32); v_required varchar(64)[];
|
||||
BEGIN
|
||||
SELECT nt.contour, nt.cta_action, ca.required_instance_fields
|
||||
INTO v_contour, v_action, v_required
|
||||
FROM han_app.notification_types nt
|
||||
JOIN han_app.notification_cta_actions ca ON ca.code=nt.cta_action
|
||||
WHERE nt.code=NEW.notification_type AND nt.record_status='A';
|
||||
IF v_contour IS DISTINCT FROM 'G' THEN
|
||||
RAISE EXCEPTION 'notification type must use guest contour';
|
||||
END IF;
|
||||
IF ('instruction_url'=ANY(v_required)) <> (NEW.instruction_url IS NOT NULL)
|
||||
OR ('chat_message_text'=ANY(v_required)) <>
|
||||
(NEW.chat_message_text IS NOT NULL) THEN
|
||||
RAISE EXCEPTION 'guest notification fields do not match CTA';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
CREATE TRIGGER trg_validate_guest_notification
|
||||
BEFORE INSERT OR UPDATE ON han_app.guest_notifications
|
||||
FOR EACH ROW EXECUTE FUNCTION han_app.validate_guest_notification();
|
||||
|
||||
CREATE OR REPLACE FUNCTION han_app.enqueue_client_document_sync()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY INVOKER
|
||||
SET search_path = han_app, pg_temp AS $$
|
||||
BEGIN
|
||||
IF current_setting('han.sync_suppress', true) = 'true' THEN RETURN NEW; END IF;
|
||||
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(), 'document.client_uploaded', 'client_document', NEW.id,
|
||||
'document.client_uploaded:' || NEW.id::text,
|
||||
jsonb_build_object(
|
||||
'client_document_id', NEW.id, 'user_id', NEW.user_id,
|
||||
'context_type', NEW.context_type, 'context_id', NEW.context_id,
|
||||
'submission_id', NEW.submission_id, 'storage_bucket', NEW.storage_bucket,
|
||||
'object_key', NEW.object_key, 'original_file_name', NEW.original_file_name,
|
||||
'mime_type', NEW.mime_type, 'size_bytes', NEW.size_bytes,
|
||||
'checksum_sha256', NEW.checksum_sha256),
|
||||
'pending', 0, now(), now(), now())
|
||||
ON CONFLICT (dedup_key) DO NOTHING;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
CREATE TRIGGER trg_client_document_sync
|
||||
AFTER INSERT ON han_app.client_documents
|
||||
FOR EACH ROW WHEN (NEW.record_status='A')
|
||||
EXECUTE FUNCTION han_app.enqueue_client_document_sync();
|
||||
"""
|
||||
)
|
||||
|
||||
_seed_catalog(bind)
|
||||
_seed_settings(bind)
|
||||
|
||||
|
||||
def _seed_catalog(bind: sa.Connection) -> None:
|
||||
# asyncpg rejects multiple SQL commands in one prepared statement.
|
||||
_execute_batch(
|
||||
"""
|
||||
INSERT INTO han_app.notification_cta_actions
|
||||
(id,code,description,requires_auth,required_instance_fields)
|
||||
VALUES
|
||||
(gen_random_uuid(),'open_detail','Open notification detail',true,ARRAY['details']),
|
||||
(gen_random_uuid(),'open_payment_url','Open payment URL',true,ARRAY['payment_url']),
|
||||
(gen_random_uuid(),'send_chat_message','Send prepared chat message',true,
|
||||
ARRAY['chat_message_text']),
|
||||
(gen_random_uuid(),'start_auth','Start authentication',false,ARRAY[]::varchar[]),
|
||||
(gen_random_uuid(),'install_app_prompt','Install application or open instruction',
|
||||
false,ARRAY['instruction_url']);
|
||||
INSERT INTO han_app.notification_buttons
|
||||
(id,code,label,sets_hidden,applies_hidden_ttl,close_reason,submits_documents)
|
||||
VALUES
|
||||
(gen_random_uuid(),'done','Готово',false,false,'user_done',false),
|
||||
(gen_random_uuid(),'later','Сделаю позже',false,false,NULL,false),
|
||||
(gen_random_uuid(),'gotit','Понятно',true,true,NULL,false),
|
||||
(gen_random_uuid(),'send_docs','Отправить документы',false,false,
|
||||
'docs_submitted',true);
|
||||
INSERT INTO han_app.notification_color_tokens
|
||||
(id,code,description,sort_order)
|
||||
VALUES
|
||||
(gen_random_uuid(),'critical','Requires immediate attention',1),
|
||||
(gen_random_uuid(),'warning','Waiting for a client action',2),
|
||||
(gen_random_uuid(),'success','Successful result',3),
|
||||
(gen_random_uuid(),'info','Informational notification',4),
|
||||
(gen_random_uuid(),'promo','Marketing offer',5),
|
||||
(gen_random_uuid(),'neutral','Neutral interface hint',6);
|
||||
"""
|
||||
)
|
||||
types = [
|
||||
(
|
||||
"authorize",
|
||||
"G",
|
||||
1,
|
||||
False,
|
||||
"Гостевой режим",
|
||||
"neutral",
|
||||
"login",
|
||||
"Войти →",
|
||||
"start_auth",
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"install_app",
|
||||
"G",
|
||||
2,
|
||||
False,
|
||||
"Приложение",
|
||||
"neutral",
|
||||
"install",
|
||||
"Установить →",
|
||||
"install_app_prompt",
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"promo_global",
|
||||
"G",
|
||||
3,
|
||||
False,
|
||||
"Акция",
|
||||
"promo",
|
||||
"promo",
|
||||
"Узнать подробнее →",
|
||||
"send_chat_message",
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"ads_global",
|
||||
"G",
|
||||
4,
|
||||
False,
|
||||
"Предложение",
|
||||
"promo",
|
||||
"offer",
|
||||
"Узнать подробнее →",
|
||||
"send_chat_message",
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"urgent",
|
||||
"P",
|
||||
1,
|
||||
True,
|
||||
"Срочно",
|
||||
"critical",
|
||||
"urgent",
|
||||
"Подробнее →",
|
||||
"open_detail",
|
||||
False,
|
||||
None,
|
||||
"done",
|
||||
"later",
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"payment_pending",
|
||||
"P",
|
||||
2,
|
||||
True,
|
||||
"Оплата",
|
||||
"warning",
|
||||
"payment",
|
||||
"Оплатить →",
|
||||
"open_payment_url",
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"docs_required",
|
||||
"P",
|
||||
2,
|
||||
True,
|
||||
"Требуются документы",
|
||||
"warning",
|
||||
"upload",
|
||||
"Загрузить документы →",
|
||||
"open_detail",
|
||||
False,
|
||||
None,
|
||||
"send_docs",
|
||||
"later",
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"docs_ready",
|
||||
"P",
|
||||
3,
|
||||
True,
|
||||
"Документы готовы",
|
||||
"success",
|
||||
"download",
|
||||
"Скачать →",
|
||||
"open_detail",
|
||||
False,
|
||||
None,
|
||||
"gotit",
|
||||
None,
|
||||
True,
|
||||
True,
|
||||
["documents"],
|
||||
),
|
||||
(
|
||||
"status_changed",
|
||||
"P",
|
||||
3,
|
||||
True,
|
||||
"Статус",
|
||||
"info",
|
||||
"status",
|
||||
"Подробнее →",
|
||||
"open_detail",
|
||||
False,
|
||||
None,
|
||||
"gotit",
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"reminder",
|
||||
"P",
|
||||
3,
|
||||
True,
|
||||
"Напоминание",
|
||||
"info",
|
||||
"reminder",
|
||||
"Подробнее →",
|
||||
"open_detail",
|
||||
False,
|
||||
None,
|
||||
"done",
|
||||
"later",
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"news",
|
||||
"P",
|
||||
4,
|
||||
True,
|
||||
"Новость",
|
||||
"info",
|
||||
"news",
|
||||
"Подробнее →",
|
||||
"open_detail",
|
||||
False,
|
||||
None,
|
||||
"gotit",
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"promo_personal",
|
||||
"P",
|
||||
5,
|
||||
True,
|
||||
"Акция",
|
||||
"promo",
|
||||
"promo",
|
||||
"Узнать подробнее →",
|
||||
"send_chat_message",
|
||||
True,
|
||||
"offer_accepted",
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
(
|
||||
"ads_personal",
|
||||
"P",
|
||||
5,
|
||||
True,
|
||||
"Предложение",
|
||||
"promo",
|
||||
"offer",
|
||||
"Узнать подробнее →",
|
||||
"send_chat_message",
|
||||
True,
|
||||
"offer_accepted",
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
),
|
||||
]
|
||||
statement = sa.text(
|
||||
"""
|
||||
INSERT INTO han_app.notification_types
|
||||
(id,code,contour,priority,countable,label,color_token,icon_code,cta_text,cta_action,
|
||||
cta_sets_hidden,cta_close_reason,button_primary_code,button_secondary_code,
|
||||
documents_allowed,hide_on_document_download,required_detail_blocks)
|
||||
VALUES
|
||||
(gen_random_uuid(),:code,:contour,:priority,:countable,:label,:color,:icon,:cta_text,
|
||||
:action,:sets_hidden,:close_reason,:primary,:secondary,:documents_allowed,
|
||||
:hide_download,:required)
|
||||
"""
|
||||
)
|
||||
for row in types:
|
||||
bind.execute(
|
||||
statement,
|
||||
dict(
|
||||
zip(
|
||||
(
|
||||
"code",
|
||||
"contour",
|
||||
"priority",
|
||||
"countable",
|
||||
"label",
|
||||
"color",
|
||||
"icon",
|
||||
"cta_text",
|
||||
"action",
|
||||
"sets_hidden",
|
||||
"close_reason",
|
||||
"primary",
|
||||
"secondary",
|
||||
"documents_allowed",
|
||||
"hide_download",
|
||||
"required",
|
||||
),
|
||||
row,
|
||||
strict=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
token = os.getenv("NOTIFICATIONS_TOKEN_PRODUCER_TEST", "")
|
||||
token_hash = (
|
||||
hashlib.sha256(token.encode()).hexdigest()
|
||||
if token
|
||||
else hashlib.sha256(b"disabled:producer_test").hexdigest()
|
||||
)
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO han_app.notification_sources
|
||||
(id,code,description,token_hash,token_rotated_at)
|
||||
VALUES (gen_random_uuid(),'producer_test','Notification Center smoke producer',
|
||||
:token_hash,now())
|
||||
"""
|
||||
),
|
||||
{"token_hash": token_hash},
|
||||
)
|
||||
|
||||
|
||||
def _seed_settings(bind: sa.Connection) -> None:
|
||||
settings = [
|
||||
("notification.home.max_items", "7", "integer", False),
|
||||
("notification.center.max_items", "15", "integer", False),
|
||||
("notification.carousel.autoplay_enabled", "false", "boolean", True),
|
||||
("notification.carousel.autoplay_interval_ms", "5000", "integer", True),
|
||||
("notification.hidden.default_ttl_days", "3", "integer", False),
|
||||
("notification.documents.max_files", "10", "integer", False),
|
||||
("notification.instruction.allowed_hosts", "chat.example.ru", "string_list", False),
|
||||
("notification.expire_job.run_at", "00:01", "string", False),
|
||||
("notification.upload_draft.ttl_days", "7", "integer", False),
|
||||
("rate_limit.notifications_read.per_user", "120/minute", "string", False),
|
||||
("rate_limit.notifications_action.per_user", "60/minute", "string", False),
|
||||
("rate_limit.notification_upload.per_user", "20/minute", "string", False),
|
||||
("rate_limit.notifications_public.per_ip", "60/minute", "string", False),
|
||||
]
|
||||
statement = sa.text(
|
||||
"""
|
||||
INSERT INTO han_app.app_settings
|
||||
(setting_key,setting_value,value_type,is_public,record_status,updated_at)
|
||||
VALUES (:key,:value,:kind,:public,'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()
|
||||
"""
|
||||
)
|
||||
for key, value, kind, public in settings:
|
||||
bind.execute(statement, {"key": key, "value": value, "kind": kind, "public": public})
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise RuntimeError("Notification Center migration is forward-only")
|
||||
Reference in New Issue
Block a user