Saltar a contenido

Especificación Técnica: trx_visitas

Esquema: fuerza_ventas
Base de Datos: crm
Tabla Legacy Origen: c063t_agenda_actividades (17.793 registros)
Propósito: Cabecera principal de actividades y visitas en ruta de la fuerza de ventas. Incorpora validación de exclusividad de destino (cliente formal vs prospecto lead) y registro georreferenciado de check-in / check-out móvil.


1. Justificación y Mejoras de Arquitectura

  • Validación Estricta de Destino Exclusivo: Restringe visitas huérfanas o ambiguas mediante CHECK (num_nonnulls(cliente_id, prospecto_id) = 1).
  • Geocercas y Auditoría GPS: Registra coordenadas de check-in y check-out en DOUBLE PRECISION, midiendo la distancia en metros respecto al local para detectar eventos fuera de rango (is_checkin_fuera_rango).
  • Aislamiento Multi-Tenant (RLS): Integra empresa_id BIGINT NOT NULL con políticas RLS obligatorias.
  • Integridad Referencial Compuesta: Clave única compuesta (empresa_id, id) y FKs compuestas hacia fuerza_ventas.mae_promotores, clientes.mae_clientes, clientes.mae_locales_cliente, clientes.mae_prospectos, clientes.cat_tipos_actividad y clientes.cat_estados_visita.
  • Trazabilidad y Auditoría: Auditoría integral (created_at, updated_at, deleted_at, created_by, updated_by) con auditoría integral.

2. Definiciones de Implementación

Table fuerza_ventas.trx_visitas {
    id                          uuid              [pk, default: `gen_random_uuid()`, note: 'Identificador único UUID']
    empresa_id                  bigint        [not null, note: 'Identificador BIGINT de la empresa en core.identidad (RLS)']
    promotor_id                 uuid              [not null, note: 'Promotor comercial asignado a la visita']
    cliente_id                  uuid              [note: 'Cliente formal visitado']
    local_id                    uuid              [note: 'Punto de venta o local físico específico']
    prospecto_id                uuid              [note: 'Prospecto lead visitado']
    tipo_actividad_id           uuid              [not null, note: 'Tipo de gestión comercial efectuada']
    estado_visita_id            uuid              [not null, note: 'Estado de ejecución de la visita']
    fecha_planificada           date              [not null, note: 'Fecha programada en la ruta comercial']
    hora_estimada_llegada       timestamptz       [note: 'Hora estimada de llegada']
    fecha_checkin               timestamptz       [note: 'Timestamp real de check-in capturado por la app']
    fecha_checkout              timestamptz       [note: 'Timestamp real de check-out capturado por la app']
    latitud_checkin             double precision  [note: 'Coordenada GPS Latitud en check-in']
    longitud_checkin            double precision  [note: 'Coordenada GPS Longitud en check-in']
    latitud_checkout            double precision  [note: 'Coordenada GPS Latitud en check-out']
    longitud_checkout           double precision  [note: 'Coordenada GPS Longitud en check-out']
    distancia_metros_checkin    numeric(8,2)      [note: 'Distancia calculada en metros respecto al local']
    is_checkin_fuera_rango      boolean           [not null, default: `false`, note: 'Indica si el check-in excedió la geocerca configurada']
    observaciones_visita        text              [note: 'Bitácora y comentarios comerciales del promotor']

    // Auditoría General
    is_activo                   boolean           [not null, default: `true`, note: 'Estado lógico del registro']
    created_at                  timestamptz       [not null, default: `now()`, note: 'Fecha de creación']
    updated_at                  timestamptz       [not null, default: `now()`, note: 'Fecha de última actualización']
    deleted_at                  timestamptz       [note: 'Fecha de eliminación lógica']
    created_by                  uuid              [note: 'Usuario creador']
    updated_by                  uuid              [note: 'Usuario modificador']

    Indexes {
        (empresa_id, id) [unique, name: 'uq_trx_visitas_empresa_id']
        (empresa_id, is_activo) [name: 'ix_trx_visitas_empresa_activo']
        (empresa_id, promotor_id, fecha_planificada) [name: 'ix_trx_visitas_empresa_promotor_fecha']
        (empresa_id, cliente_id) [name: 'ix_trx_visitas_empresa_cliente']
        (empresa_id, prospecto_id) [name: 'ix_trx_visitas_empresa_prospecto']
        (empresa_id, estado_visita_id) [name: 'ix_trx_visitas_empresa_estado']
    }
}

Ref: fuerza_ventas.trx_visitas.(empresa_id, promotor_id) > fuerza_ventas.mae_promotores.(empresa_id, id)
Ref: fuerza_ventas.trx_visitas.(empresa_id, cliente_id) > clientes.mae_clientes.(empresa_id, id)
Ref: fuerza_ventas.trx_visitas.(empresa_id, local_id) > clientes.mae_locales_cliente.(empresa_id, id)
Ref: fuerza_ventas.trx_visitas.(empresa_id, prospecto_id) > clientes.mae_prospectos.(empresa_id, id)
Ref: fuerza_ventas.trx_visitas.(empresa_id, tipo_actividad_id) > clientes.cat_tipos_actividad.(empresa_id, id)
Ref: fuerza_ventas.trx_visitas.(empresa_id, estado_visita_id) > clientes.cat_estados_visita.(empresa_id, id)
CREATE TABLE IF NOT EXISTS fuerza_ventas.trx_visitas (
    id UUID NOT NULL DEFAULT gen_random_uuid(),
    empresa_id BIGINT NOT NULL,
    promotor_id UUID NOT NULL,
    cliente_id UUID,
    local_id UUID,
    prospecto_id UUID,
    tipo_actividad_id UUID NOT NULL,
    estado_visita_id UUID NOT NULL,
    fecha_planificada DATE NOT NULL,
    hora_estimada_llegada TIMESTAMPTZ,
    fecha_checkin TIMESTAMPTZ,
    fecha_checkout TIMESTAMPTZ,
    latitud_checkin DOUBLE PRECISION,
    longitud_checkin DOUBLE PRECISION,
    latitud_checkout DOUBLE PRECISION,
    longitud_checkout DOUBLE PRECISION,
    distancia_metros_checkin NUMERIC(8,2),
    is_checkin_fuera_rango BOOLEAN NOT NULL DEFAULT false,
    observaciones_visita TEXT,

    -- Auditoría General
    is_activo BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at TIMESTAMPTZ,
    created_by UUID,
    updated_by UUID,

    CONSTRAINT pk_trx_visitas
        PRIMARY KEY (id),

    CONSTRAINT uq_trx_visitas_empresa_id
        UNIQUE (empresa_id, id),

    CONSTRAINT ck_trx_visitas_destino_exclusivo
        CHECK (num_nonnulls(cliente_id, prospecto_id) = 1),

    CONSTRAINT fk_trx_visitas_promotor
        FOREIGN KEY (empresa_id, promotor_id)
        REFERENCES fuerza_ventas.mae_promotores (empresa_id, id),

    CONSTRAINT fk_trx_visitas_cliente
        FOREIGN KEY (empresa_id, cliente_id)
        REFERENCES clientes.mae_clientes (empresa_id, id),

    CONSTRAINT fk_trx_visitas_local
        FOREIGN KEY (empresa_id, local_id)
        REFERENCES clientes.mae_locales_cliente (empresa_id, id),

    CONSTRAINT fk_trx_visitas_prospecto
        FOREIGN KEY (empresa_id, prospecto_id)
        REFERENCES clientes.mae_prospectos (empresa_id, id),

    CONSTRAINT fk_trx_visitas_tipo_actividad
        FOREIGN KEY (empresa_id, tipo_actividad_id)
        REFERENCES clientes.cat_tipos_actividad (empresa_id, id),

    CONSTRAINT fk_trx_visitas_estado
        FOREIGN KEY (empresa_id, estado_visita_id)
        REFERENCES clientes.cat_estados_visita (empresa_id, id)
);

CREATE INDEX IF NOT EXISTS ix_trx_visitas_empresa_activo
    ON fuerza_ventas.trx_visitas (empresa_id, is_activo);

CREATE INDEX IF NOT EXISTS ix_trx_visitas_empresa_promotor_fecha
    ON fuerza_ventas.trx_visitas (empresa_id, promotor_id, fecha_planificada);

CREATE INDEX IF NOT EXISTS ix_trx_visitas_empresa_cliente
    ON fuerza_ventas.trx_visitas (empresa_id, cliente_id);

CREATE INDEX IF NOT EXISTS ix_trx_visitas_empresa_prospecto
    ON fuerza_ventas.trx_visitas (empresa_id, prospecto_id);

CREATE INDEX IF NOT EXISTS ix_trx_visitas_empresa_estado
    ON fuerza_ventas.trx_visitas (empresa_id, estado_visita_id);

COMMENT ON TABLE fuerza_ventas.trx_visitas IS
    'Transacción de Visitas y Agenda en Campo: Cabecera principal de actividades en ruta con validación de destino y check-in móvil.';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.id IS
    'Identificador único UUID';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.empresa_id IS
    'Identificador UUID de la empresa en core.identidad (RLS)';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.promotor_id IS
    'Promotor comercial asignado a la visita';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.cliente_id IS
    'Cliente formal visitado';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.local_id IS
    'Punto de venta o local físico específico';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.prospecto_id IS
    'Prospecto lead visitado';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.tipo_actividad_id IS
    'Tipo de gestión comercial efectuada';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.estado_visita_id IS
    'Estado de ejecución de la visita';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.fecha_planificada IS
    'Fecha programada en la ruta comercial';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.hora_estimada_llegada IS
    'Hora estimada de llegada';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.fecha_checkin IS
    'Timestamp real de check-in capturado por la app';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.fecha_checkout IS
    'Timestamp real de check-out capturado por la app';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.latitud_checkin IS
    'Coordenada GPS Latitud en check-in';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.longitud_checkin IS
    'Coordenada GPS Longitud en check-in';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.latitud_checkout IS
    'Coordenada GPS Latitud en check-out';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.longitud_checkout IS
    'Coordenada GPS Longitud en check-out';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.distancia_metros_checkin IS
    'Distancia calculada en metros respecto al local';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.is_checkin_fuera_rango IS
    'Indica si el check-in excedió la geocerca configurada';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.observaciones_visita IS
    'Bitácora y comentarios comerciales del promotor';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.is_activo IS
    'Estado lógico del registro';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.created_at IS
    'Fecha de creación';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.updated_at IS
    'Fecha de última actualización';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.deleted_at IS
    'Fecha de eliminación lógica';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.created_by IS
    'Usuario creador';

COMMENT ON COLUMN fuerza_ventas.trx_visitas.updated_by IS
    'Usuario modificador';

ALTER TABLE fuerza_ventas.trx_visitas
    ENABLE ROW LEVEL SECURITY;

CREATE POLICY rls_trx_visitas_empresa
    ON fuerza_ventas.trx_visitas
    FOR ALL
    USING (
        empresa_id = current_setting(
            'app.current_empresa_id',
            true
        )::BIGINT
    )
    WITH CHECK (
        empresa_id = current_setting(
            'app.current_empresa_id',
            true
        )::BIGINT
    );
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Optional

from sqlalchemy import (
    BigInteger,
    Boolean,
    CheckConstraint,
    Date,
    Double,
    ForeignKeyConstraint,
    Index,
    Numeric,
    Text,
    UniqueConstraint,
    text,
)
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ, UUID
from sqlalchemy.orm import Mapped, mapped_column

from app.db.base import Base


class TrxVisitas(Base):
    """
    Transacción de Visitas y Agenda en Campo.

    Cabecera principal de actividades en ruta de la fuerza de ventas.
    Incorpora validación de exclusividad de destino (cliente vs prospecto)
    y trazabilidad completa de check-in / check-out georreferenciado.

    Legacy:
        c063t_agenda_actividades
    """

    __tablename__ = "trx_visitas"

    __table_args__ = (
        UniqueConstraint(
            "empresa_id",
            "id",
            name="uq_trx_visitas_empresa_id",
        ),
        CheckConstraint(
            "num_nonnulls(cliente_id, prospecto_id) = 1",
            name="ck_trx_visitas_destino_exclusivo",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "promotor_id"],
            ["fuerza_ventas.mae_promotores.empresa_id", "fuerza_ventas.mae_promotores.id"],
            name="fk_trx_visitas_promotor",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "cliente_id"],
            ["clientes.mae_clientes.empresa_id", "clientes.mae_clientes.id"],
            name="fk_trx_visitas_cliente",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "local_id"],
            ["clientes.mae_locales_cliente.empresa_id", "clientes.mae_locales_cliente.id"],
            name="fk_trx_visitas_local",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "prospecto_id"],
            ["clientes.mae_prospectos.empresa_id", "clientes.mae_prospectos.id"],
            name="fk_trx_visitas_prospecto",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "tipo_actividad_id"],
            ["clientes.cat_tipos_actividad.empresa_id", "clientes.cat_tipos_actividad.id"],
            name="fk_trx_visitas_tipo_actividad",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "estado_visita_id"],
            ["clientes.cat_estados_visita.empresa_id", "clientes.cat_estados_visita.id"],
            name="fk_trx_visitas_estado",
        ),
        Index(
            "ix_trx_visitas_empresa_activo",
            "empresa_id",
            "is_activo",
        ),
        Index(
            "ix_trx_visitas_empresa_promotor_fecha",
            "empresa_id",
            "promotor_id",
            "fecha_planificada",
        ),
        Index(
            "ix_trx_visitas_empresa_cliente",
            "empresa_id",
            "cliente_id",
        ),
        Index(
            "ix_trx_visitas_empresa_prospecto",
            "empresa_id",
            "prospecto_id",
        ),
        Index(
            "ix_trx_visitas_empresa_estado",
            "empresa_id",
            "estado_visita_id",
        ),
        {
            "schema": "fuerza_ventas",
            "comment": "Transacción de Visitas y Agenda en Campo",
        },
    )

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        primary_key=True,
        server_default=text("gen_random_uuid()"),
        comment="Identificador único UUID",
    )

    empresa_id: Mapped[int] = mapped_column(
        BigInteger,
        nullable=False,
        comment="Identificador UUID de la empresa en core.identidad (RLS)",
    )

    promotor_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Promotor comercial asignado a la visita",
    )

    cliente_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Cliente formal visitado",
    )

    local_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Punto de venta o local físico específico",
    )

    prospecto_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Prospecto lead visitado",
    )

    tipo_actividad_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Tipo de gestión comercial efectuada",
    )

    estado_visita_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Estado de ejecución de la visita",
    )

    fecha_planificada: Mapped[date] = mapped_column(
        Date,
        nullable=False,
        comment="Fecha programada en la ruta comercial",
    )

    hora_estimada_llegada: Mapped[Optional[datetime]] = mapped_column(
        TIMESTAMPTZ,
        nullable=True,
        comment="Hora estimada de llegada",
    )

    fecha_checkin: Mapped[Optional[datetime]] = mapped_column(
        TIMESTAMPTZ,
        nullable=True,
        comment="Timestamp real de check-in capturado por la app",
    )

    fecha_checkout: Mapped[Optional[datetime]] = mapped_column(
        TIMESTAMPTZ,
        nullable=True,
        comment="Timestamp real de check-out capturado por la app",
    )

    latitud_checkin: Mapped[Optional[float]] = mapped_column(
        Double,
        nullable=True,
        comment="Coordenada GPS Latitud en check-in",
    )

    longitud_checkin: Mapped[Optional[float]] = mapped_column(
        Double,
        nullable=True,
        comment="Coordenada GPS Longitud en check-in",
    )

    latitud_checkout: Mapped[Optional[float]] = mapped_column(
        Double,
        nullable=True,
        comment="Coordenada GPS Latitud en check-out",
    )

    longitud_checkout: Mapped[Optional[float]] = mapped_column(
        Double,
        nullable=True,
        comment="Coordenada GPS Longitud en check-out",
    )

    distancia_metros_checkin: Mapped[Optional[Decimal]] = mapped_column(
        Numeric(8, 2),
        nullable=True,
        comment="Distancia calculada en metros respecto al local",
    )

    is_checkin_fuera_rango: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        server_default=text("false"),
        comment="Indica si el check-in excedió la geocerca configurada",
    )

    observaciones_visita: Mapped[Optional[str]] = mapped_column(
        Text,
        nullable=True,
        comment="Bitácora y comentarios comerciales del promotor",
    )

    is_activo: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        server_default=text("true"),
        comment="Estado lógico del registro",
    )

    created_at: Mapped[datetime] = mapped_column(
        TIMESTAMPTZ,
        nullable=False,
        server_default=text("now()"),
        comment="Fecha de creación",
    )

    updated_at: Mapped[datetime] = mapped_column(
        TIMESTAMPTZ,
        nullable=False,
        server_default=text("now()"),
        comment="Fecha de última actualización",
    )

    deleted_at: Mapped[Optional[datetime]] = mapped_column(
        TIMESTAMPTZ,
        nullable=True,
        comment="Fecha de eliminación lógica",
    )

    created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Usuario creador",
    )

    updated_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Usuario modificador",
    )
"""create trx_visitas

Revision ID: trx_0001
Revises: mae_0010
Create Date: 2026-09-09
"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


# revision identifiers, used by Alembic.
revision: str = "trx_0001"
down_revision: Union[str, Sequence[str], None] = "mae_0010"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:

    op.create_table(
        "trx_visitas",

        sa.Column(
            "id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            server_default=sa.text("gen_random_uuid()"),
            comment="Identificador único UUID",
        ),

        sa.Column(
            "empresa_id",
            sa.BigInteger(),
            nullable=False,
            comment="Identificador UUID de la empresa en core.identidad (RLS)",
        ),

        sa.Column(
            "promotor_id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            comment="Promotor comercial asignado a la visita",
        ),

        sa.Column(
            "cliente_id",
            postgresql.UUID(as_uuid=True),
            nullable=True,
            comment="Cliente formal visitado",
        ),

        sa.Column(
            "local_id",
            postgresql.UUID(as_uuid=True),
            nullable=True,
            comment="Punto de venta o local físico específico",
        ),

        sa.Column(
            "prospecto_id",
            postgresql.UUID(as_uuid=True),
            nullable=True,
            comment="Prospecto lead visitado",
        ),

        sa.Column(
            "tipo_actividad_id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            comment="Tipo de gestión comercial efectuada",
        ),

        sa.Column(
            "estado_visita_id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            comment="Estado de ejecución de la visita",
        ),

        sa.Column(
            "fecha_planificada",
            sa.Date(),
            nullable=False,
            comment="Fecha programada en la ruta comercial",
        ),

        sa.Column(
            "hora_estimada_llegada",
            sa.TIMESTAMP(timezone=True),
            nullable=True,
            comment="Hora estimada de llegada",
        ),

        sa.Column(
            "fecha_checkin",
            sa.TIMESTAMP(timezone=True),
            nullable=True,
            comment="Timestamp real de check-in capturado por la app",
        ),

        sa.Column(
            "fecha_checkout",
            sa.TIMESTAMP(timezone=True),
            nullable=True,
            comment="Timestamp real de check-out capturado por la app",
        ),

        sa.Column(
            "latitud_checkin",
            sa.Float(precision=53),
            nullable=True,
            comment="Coordenada GPS Latitud en check-in",
        ),

        sa.Column(
            "longitud_checkin",
            sa.Float(precision=53),
            nullable=True,
            comment="Coordenada GPS Longitud en check-in",
        ),

        sa.Column(
            "latitud_checkout",
            sa.Float(precision=53),
            nullable=True,
            comment="Coordenada GPS Latitud en check-out",
        ),

        sa.Column(
            "longitud_checkout",
            sa.Float(precision=53),
            nullable=True,
            comment="Coordenada GPS Longitud en check-out",
        ),

        sa.Column(
            "distancia_metros_checkin",
            sa.Numeric(precision=8, scale=2),
            nullable=True,
            comment="Distancia calculada en metros respecto al local",
        ),

        sa.Column(
            "is_checkin_fuera_rango",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("false"),
            comment="Indica si el check-in excedió la geocerca configurada",
        ),

        sa.Column(
            "observaciones_visita",
            sa.Text(),
            nullable=True,
            comment="Bitácora y comentarios comerciales del promotor",
        ),

        # Auditoría General
        sa.Column(
            "is_activo",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("true"),
            comment="Estado lógico del registro",
        ),

        sa.Column(
            "created_at",
            sa.TIMESTAMP(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
            comment="Fecha de creación",
        ),

        sa.Column(
            "updated_at",
            sa.TIMESTAMP(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
            comment="Fecha de última actualización",
        ),

        sa.Column(
            "deleted_at",
            sa.TIMESTAMP(timezone=True),
            nullable=True,
            comment="Fecha de eliminación lógica",
        ),

        sa.Column(
            "created_by",
            postgresql.UUID(as_uuid=True),
            nullable=True,
            comment="Usuario creador",
        ),

        sa.Column(
            "updated_by",
            postgresql.UUID(as_uuid=True),
            nullable=True,
            comment="Usuario modificador",
        ),

        sa.PrimaryKeyConstraint(
            "id",
            name="pk_trx_visitas",
        ),

        sa.UniqueConstraint(
            "empresa_id",
            "id",
            name="uq_trx_visitas_empresa_id",
        ),

        sa.CheckConstraint(
            "num_nonnulls(cliente_id, prospecto_id) = 1",
            name="ck_trx_visitas_destino_exclusivo",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "promotor_id"],
            ["fuerza_ventas.mae_promotores.empresa_id", "fuerza_ventas.mae_promotores.id"],
            name="fk_trx_visitas_promotor",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "cliente_id"],
            ["clientes.mae_clientes.empresa_id", "clientes.mae_clientes.id"],
            name="fk_trx_visitas_cliente",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "local_id"],
            ["clientes.mae_locales_cliente.empresa_id", "clientes.mae_locales_cliente.id"],
            name="fk_trx_visitas_local",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "prospecto_id"],
            ["clientes.mae_prospectos.empresa_id", "clientes.mae_prospectos.id"],
            name="fk_trx_visitas_prospecto",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "tipo_actividad_id"],
            ["clientes.cat_tipos_actividad.empresa_id", "clientes.cat_tipos_actividad.id"],
            name="fk_trx_visitas_tipo_actividad",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "estado_visita_id"],
            ["clientes.cat_estados_visita.empresa_id", "clientes.cat_estados_visita.id"],
            name="fk_trx_visitas_estado",
        ),

        comment="Transacción de Visitas y Agenda en Campo",
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_visitas_empresa_activo",
        "trx_visitas",
        ["empresa_id", "is_activo"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_visitas_empresa_promotor_fecha",
        "trx_visitas",
        ["empresa_id", "promotor_id", "fecha_planificada"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_visitas_empresa_cliente",
        "trx_visitas",
        ["empresa_id", "cliente_id"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_visitas_empresa_prospecto",
        "trx_visitas",
        ["empresa_id", "prospecto_id"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_visitas_empresa_estado",
        "trx_visitas",
        ["empresa_id", "estado_visita_id"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.execute(
        """
        ALTER TABLE fuerza_ventas.trx_visitas
        ENABLE ROW LEVEL SECURITY;
        """
    )

    op.execute(
        """
        CREATE POLICY rls_trx_visitas_empresa
        ON fuerza_ventas.trx_visitas
        FOR ALL
        USING (
            empresa_id = current_setting(
                'app.current_empresa_id',
                true
            )::BIGINT
        )
        WITH CHECK (
            empresa_id = current_setting(
                'app.current_empresa_id',
                true
            )::BIGINT
        );
        """
    )


def downgrade() -> None:

    op.execute(
        """
        DROP POLICY IF EXISTS rls_trx_visitas_empresa
        ON fuerza_ventas.trx_visitas;
        """
    )

    op.execute(
        """
        ALTER TABLE fuerza_ventas.trx_visitas
        DISABLE ROW LEVEL SECURITY;
        """
    )

    op.drop_index(
        "ix_trx_visitas_empresa_estado",
        table_name="trx_visitas",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_visitas_empresa_prospecto",
        table_name="trx_visitas",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_visitas_empresa_cliente",
        table_name="trx_visitas",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_visitas_empresa_promotor_fecha",
        table_name="trx_visitas",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_visitas_empresa_activo",
        table_name="trx_visitas",
        schema="fuerza_ventas",
    )

    op.drop_table(
        "trx_visitas",
        schema="fuerza_ventas",
    )