"""Create SMS journal schema objects. Revision ID: 0001_initial """ import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision = "0001_initial" down_revision = None branch_labels = None depends_on = None SCHEMA = "sms" channel = postgresql.ENUM("SMS", name="sms_channel", schema=SCHEMA, create_type=False) send_status = postgresql.ENUM( "pending", "accepted", "rejected", "failed", "uncertain", "skipped", name="sms_send_status", schema=SCHEMA, create_type=False, ) delivery_status = postgresql.ENUM( "unknown", "sent", "delivered", "undelivered", "unsent", name="sms_delivery_status", schema=SCHEMA, create_type=False, ) def upgrade() -> None: bind = op.get_bind() postgresql.ENUM("SMS", name="sms_channel", schema=SCHEMA).create(bind) postgresql.ENUM( "pending", "accepted", "rejected", "failed", "uncertain", "skipped", name="sms_send_status", schema=SCHEMA, ).create(bind) postgresql.ENUM( "unknown", "sent", "delivered", "undelivered", "unsent", name="sms_delivery_status", schema=SCHEMA, ).create(bind) op.create_table( "sms_template", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("code", sa.String(64), nullable=False), sa.Column("channel", channel, nullable=False), sa.Column("locale", sa.String(16), nullable=False), sa.Column("version", sa.Integer(), nullable=False), sa.Column("body_template", sa.Text(), nullable=False), sa.Column("placeholders", postgresql.JSONB(), nullable=False), sa.Column("sender_name", sa.String(64)), sa.Column("max_parts", sa.SmallInteger(), nullable=False), sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column("approved_at", sa.DateTime(timezone=True)), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.Column("created_by", sa.String(64), nullable=False), sa.CheckConstraint("version > 0", name="ck_template_version_positive"), sa.CheckConstraint("max_parts BETWEEN 1 AND 10", name="ck_template_max_parts"), sa.UniqueConstraint("code", "channel", "locale", "version", name="uq_template_version"), schema=SCHEMA, ) op.create_index( "uq_template_active", "sms_template", ["code", "channel", "locale"], unique=True, schema=SCHEMA, postgresql_where=sa.text("is_active"), ) op.create_table( "sms_setting", sa.Column("setting_key", sa.String(128), primary_key=True), sa.Column("setting_value", postgresql.JSONB(), nullable=False), sa.Column("value_type", sa.String(16), nullable=False), sa.Column("description", sa.Text(), nullable=False), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.CheckConstraint( "value_type IN ('string','integer','boolean')", name="ck_setting_value_type" ), schema=SCHEMA, ) op.create_table( "sms_outbound_message", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.Column("requested_at", sa.DateTime(timezone=True), nullable=False), sa.Column("accepted_at", sa.DateTime(timezone=True)), sa.Column("sent_at", sa.DateTime(timezone=True)), sa.Column("delivered_at", sa.DateTime(timezone=True)), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.Column("requester_service", sa.String(64), nullable=False), sa.Column("process", sa.String(64), nullable=False), sa.Column("channel", sa.String(16), nullable=False), sa.Column("provider", sa.String(32), nullable=False), sa.Column("phone_e164", sa.String(16), nullable=False), sa.Column("phone_digits", sa.String(15), nullable=False), sa.Column("phone_masked", sa.String(32), nullable=False), sa.Column( "template_id", postgresql.UUID(as_uuid=True), sa.ForeignKey(f"{SCHEMA}.sms_template.id"), nullable=False, ), sa.Column("template_code", sa.String(64), nullable=False), sa.Column("body_rendered", sa.Text(), nullable=False), sa.Column("substitutions", postgresql.JSONB(), nullable=False), sa.Column("send_status", send_status, nullable=False), sa.Column("delivery_status", delivery_status, nullable=False), sa.Column("provider_message_id", sa.String(128)), sa.Column("provider_external_id", sa.String(128)), sa.Column("customer_ref", sa.String(128)), sa.Column("idempotency_key", sa.String(192), nullable=False), sa.Column("request_fingerprint", sa.String(64), nullable=False), sa.Column("request_id", sa.String(128)), sa.Column("traceparent", sa.String(55)), sa.Column("provider_http_status", sa.Integer()), sa.Column("provider_error_code", sa.String(64)), sa.Column("provider_error_message", sa.String(256)), sa.Column("sender_name", sa.String(64), nullable=False), sa.Column("message_ttl_sec", sa.Integer()), sa.Column("attempt_count", sa.Integer(), nullable=False, server_default="0"), sa.Column("last_attempt_at", sa.DateTime(timezone=True)), sa.Column("next_attempt_at", sa.DateTime(timezone=True)), sa.Column("worker_locked_until", sa.DateTime(timezone=True)), sa.Column("parts", sa.Integer()), sa.Column("price", sa.Numeric(14, 4)), sa.Column("currency", sa.String(3)), sa.Column("callback_last_at", sa.DateTime(timezone=True)), sa.CheckConstraint("channel = 'SMS'", name="ck_outbound_channel"), sa.CheckConstraint("provider = 'idgtl'", name="ck_outbound_provider"), sa.CheckConstraint("process = 'auth_otp'", name="ck_outbound_process"), sa.CheckConstraint("message_ttl_sec BETWEEN 60 AND 86400", name="ck_outbound_ttl"), sa.CheckConstraint("attempt_count >= 0", name="ck_outbound_attempts"), sa.UniqueConstraint("requester_service", "idempotency_key", name="uq_outbound_idempotency"), schema=SCHEMA, ) op.create_index( "uq_outbound_provider_message", "sms_outbound_message", ["provider", "provider_message_id"], unique=True, schema=SCHEMA, postgresql_where=sa.text("provider_message_id IS NOT NULL"), ) op.create_index( "ix_outbound_phone_created", "sms_outbound_message", ["phone_e164", sa.text("created_at DESC")], schema=SCHEMA, ) op.create_index( "ix_outbound_requester_process_created", "sms_outbound_message", ["requester_service", "process", sa.text("created_at DESC")], schema=SCHEMA, ) op.create_index( "ix_outbound_customer_ref", "sms_outbound_message", ["customer_ref"], schema=SCHEMA ) op.create_index( "ix_outbound_send_created", "sms_outbound_message", ["send_status", "created_at"], schema=SCHEMA, ) op.create_index( "ix_outbound_delivery_updated", "sms_outbound_message", ["delivery_status", "updated_at"], schema=SCHEMA, ) op.create_table( "sms_callback_event", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("message_uuid", sa.String(128), nullable=False), sa.Column("callback_event", sa.String(32), nullable=False), sa.Column("status", sa.String(32), nullable=False), sa.Column("status_time", sa.DateTime(timezone=True), nullable=False), sa.Column( "received_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.UniqueConstraint( "message_uuid", "callback_event", "status", "status_time", name="uq_callback_event" ), schema=SCHEMA, ) def downgrade() -> None: op.drop_table("sms_callback_event", schema=SCHEMA) op.drop_table("sms_outbound_message", schema=SCHEMA) op.drop_table("sms_setting", schema=SCHEMA) op.drop_table("sms_template", schema=SCHEMA) delivery_status.drop(op.get_bind()) send_status.drop(op.get_bind()) channel.drop(op.get_bind())