"""Day-1 Feishu welcome card — push-only outbox marker + catch-up cron. 1. ``notification_outbox.hidden_from_inbox`` (TINYINT/BOOL, default 0): marks a row as a *push-only* delivery record — a follow-up push whose content the recipient already holds as another inbox row (the Day-1 Feishu welcome card that trails the Day-1 welcome email). Personal inbox/bell queries exclude these; admin outbox surfaces keep them. 2. Seeds the ``hr.hire_day_feishu_welcome`` scheduled job (every 30 minutes): new hires get the welcome EMAIL at 09:00 on their hire date, but their Feishu account is only minted minutes later by the identity-sync ``feishu_contact`` push — a Feishu DM at email time dead-letters on "open_id not found" (a permanent, non-retried error; the reason PR #1572 made the welcome email-only). This sweep sends the rich Feishu welcome card once the account exists. Revision ID: 375 Revises: 374 """ import uuid import sqlalchemy as sa from alembic import op revision = "375" down_revision = "374" branch_labels = None depends_on = None JOB = "hr.hire_day_feishu_welcome" def upgrade() -> None: op.add_column( "notification_outbox", sa.Column( "hidden_from_inbox", sa.Boolean(), nullable=False, server_default="0", ), ) bind = op.get_bind() job_exists = bind.execute( sa.text("SELECT 1 FROM scheduled_jobs WHERE name = :n LIMIT 1"), {"n": JOB}, ).scalar() if not job_exists: bind.execute( sa.text( """ INSERT INTO scheduled_jobs (id, name, display_name, description, handler_key, trigger_type, interval_seconds, cron_expr, timezone, enabled, config, run_count, failure_count, created_at, updated_at) VALUES (:id, :name, :display_name, :description, :name, 'interval', 1800, NULL, 'UTC', 1, '{}', 0, 0, NOW(), NOW()) """ ), { "id": str(uuid.uuid4()), "name": JOB, "display_name": "入职欢迎 · 飞书卡片补发", "description": ( "每 30 分钟扫一次最近 3 天已发过 Day-1 欢迎邮件、" "但飞书欢迎卡片还没送达的新员工;飞书账号一建好" "(identity-sync feishu_contact)就补发富卡片版第一天指南。" "幂等键 identity_persons.it_info_sent_channels。" ), }, ) def downgrade() -> None: bind = op.get_bind() bind.execute(sa.text("DELETE FROM scheduled_jobs WHERE name = :n"), {"n": JOB}) op.drop_column("notification_outbox", "hidden_from_inbox")