Saltar a contenido

Especificación Técnica: mae_promotores

Esquema: fuerza_ventas
Base de Datos: crm
Tabla Legacy Origen: i127t_promotor (61 col)
Propósito: Perfil comercial del promotor de ventas. Aísla la identidad de usuario y credenciales (delegadas a core.identidad) y se enfoca en parámetros comerciales, metas, comisiones y cupos de crédito comercial.


1. Justificación y Mejoras de Arquitectura

  • Desacoplamiento de Identidad: La autenticación y perfiles de usuario residen en core.identidad. La tabla comercial conserva únicamente atributos del rol operativo de ventas (usuario_id y persona_id correlacionados de forma lógica sin foreign keys cross-database).
  • Integración con Catálogos de Comisión: Asocia la modalidad de liquidación de comisiones mediante FK compuesta (empresa_id, modalidad_pago_comision_id) hacia fuerza_ventas.cat_modalidades_pago_comision(empresa_id, id).
  • Aislamiento Multi-Tenant (RLS): Integra empresa_id BIGINT NOT NULL con políticas RLS obligatorias para segregar la fuerza de ventas por tenant.
  • Soporte de Relaciones Compuestas: Clave única compuesta (empresa_id, id) que permite a las entidades hijas (mae_agencias, mae_equipos_consignacion, mae_metas_promotores, mae_presupuestos_cliente, trx_visitas) heredar la restricción de tenant en sus foreign keys.
  • Trazabilidad y Auditoría: Auditoría completa y auditoría integral.

2. Definiciones de Implementación

Table fuerza_ventas.mae_promotores {
    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)']
    usuario_id                  uuid          [not null, note: 'Correlación con core.identidad.mae_usuarios (sin FK cross-DB)']
    persona_id                  uuid          [not null, note: 'Correlación con core.identidad.mae_personas (sin FK cross-DB)']
    codigo_promotor             varchar(30)   [not null, note: 'Código interno comercial']
    tipo_promotor_id            uuid          [not null, note: 'Tipo de promotor (FK cat_tipos_promotor)']
    modalidad_pago_comision_id  uuid          [note: 'Modalidad de pago de comisión (FK cat_modalidades_pago_comision)']
    is_coordinador              boolean       [not null, default: `false`, note: 'Indica si tiene rol de coordinación/supervisión']
    is_casa_matriz              boolean       [not null, default: `false`, note: 'Indica si opera desde sede principal']
    porcentaje_comision         numeric(5,2)  [not null, default: `0.00`, note: 'Porcentaje base de comisión']
    monto_cupo_maximo_credito   numeric(12,2) [not null, default: `0.00`, note: 'Cupo máximo acumulado otorgable a su cartera']
    dispositivo_imei             varchar(50)   [note: 'Identificador del dispositivo móvil autorizado']
    telefono_corporativo        varchar(50)   [note: 'Número móvil institucional']

    // 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_mae_promotores_empresa_id']
        (empresa_id, codigo_promotor) [unique, name: 'uq_mae_promotores_empresa_codigo']
        (empresa_id, usuario_id) [unique, name: 'uq_mae_promotores_empresa_usuario']
        (empresa_id, persona_id) [name: 'ix_mae_promotores_empresa_persona']
        (empresa_id, modalidad_pago_comision_id) [name: 'ix_mae_promotores_empresa_modalidad']
        (empresa_id, tipo_promotor) [name: 'ix_mae_promotores_empresa_tipo']
        (empresa_id, is_activo) [name: 'ix_mae_promotores_empresa_activo']
    }
}

Ref: fuerza_ventas.mae_promotores.(empresa_id, modalidad_pago_comision_id) > fuerza_ventas.cat_modalidades_pago_comision.(empresa_id, id) [delete: restrict]
Ref: fuerza_ventas.mae_promotores.(empresa_id, tipo_promotor_id) > fuerza_ventas.cat_tipos_promotor.(empresa_id, id) [delete: restrict]
CREATE TABLE IF NOT EXISTS fuerza_ventas.mae_promotores (
    id UUID NOT NULL DEFAULT gen_random_uuid(),
    empresa_id BIGINT NOT NULL,
    usuario_id UUID NOT NULL,
    persona_id UUID NOT NULL,
    codigo_promotor VARCHAR(30) NOT NULL,
    tipo_promotor VARCHAR(50) NOT NULL DEFAULT 'VENDEDOR',
    modalidad_pago_comision_id UUID,
    is_coordinador BOOLEAN NOT NULL DEFAULT false,
    is_casa_matriz BOOLEAN NOT NULL DEFAULT false,
    porcentaje_comision NUMERIC(5,2) NOT NULL DEFAULT 0.00,
    monto_cupo_maximo_credito NUMERIC(12,2) NOT NULL DEFAULT 0.00,
    dispositivo_imei VARCHAR(50),
    telefono_corporativo VARCHAR(50),

    -- 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_mae_promotores PRIMARY KEY (id),
    CONSTRAINT uq_mae_promotores_empresa_id UNIQUE (empresa_id, id),
    CONSTRAINT uq_mae_promotores_empresa_codigo UNIQUE (empresa_id, codigo_promotor),
    CONSTRAINT uq_mae_promotores_empresa_usuario UNIQUE (empresa_id, usuario_id),
    CONSTRAINT fk_mae_promotores_modalidad_comision FOREIGN KEY (empresa_id, modalidad_pago_comision_id)
        REFERENCES fuerza_ventas.cat_modalidades_pago_comision (empresa_id, id)
        ON DELETE RESTRICT
);

-- Índices
CREATE INDEX IF NOT EXISTS ix_mae_promotores_empresa_persona
    ON fuerza_ventas.mae_promotores (empresa_id, persona_id);

CREATE INDEX IF NOT EXISTS ix_mae_promotores_empresa_modalidad
    ON fuerza_ventas.mae_promotores (empresa_id, modalidad_pago_comision_id);

CREATE INDEX IF NOT EXISTS ix_mae_promotores_empresa_tipo
    ON fuerza_ventas.mae_promotores (empresa_id, tipo_promotor);

CREATE INDEX IF NOT EXISTS ix_mae_promotores_empresa_activo
    ON fuerza_ventas.mae_promotores (empresa_id, is_activo);

-- Row-Level Security
ALTER TABLE fuerza_ventas.mae_promotores ENABLE ROW LEVEL SECURITY;

CREATE POLICY rls_mae_promotores_empresa
    ON fuerza_ventas.mae_promotores
    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
    );

-- Comentarios
COMMENT ON TABLE fuerza_ventas.mae_promotores IS 'Maestro de Promotores y Asesores Comerciales';
COMMENT ON COLUMN fuerza_ventas.mae_promotores.modalidad_pago_comision_id IS 'Identificador de la modalidad de pago de comisión (FK cat_modalidades_pago_comision)';
COMMENT ON COLUMN fuerza_ventas.mae_promotores.usuario_id IS 'Correlación con core.identidad.mae_usuarios (sin FK cross-DB)';
COMMENT ON COLUMN fuerza_ventas.mae_promotores.persona_id IS 'Correlación con core.identidad.mae_personas (sin FK cross-DB)';
COMMENT ON COLUMN fuerza_ventas.mae_promotores.tipo_promotor IS 'VENDEDOR, PREVENTISTA, AUTOVENTA, COBRADOR, MERCHANDISER, SUPERVISOR';
from datetime import datetime
from typing import Optional
import uuid

from sqlalchemy import (
    BigInteger,
    Boolean,
    ForeignKeyConstraint,
    Index,
    Numeric,
    String,
    TIMESTAMPTZ,
    UniqueConstraint,
    text,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship


class Base(DeclarativeBase):
    pass


class MaePromotor(Base):
    __tablename__ = "mae_promotores"
    __table_args__ = (
        UniqueConstraint(
            "empresa_id",
            "id",
            name="uq_mae_promotores_empresa_id",
        ),
        UniqueConstraint(
            "empresa_id",
            "codigo_promotor",
            name="uq_mae_promotores_empresa_codigo",
        ),
        UniqueConstraint(
            "empresa_id",
            "usuario_id",
            name="uq_mae_promotores_empresa_usuario",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "modalidad_pago_comision_id"],
            ["fuerza_ventas.cat_modalidades_pago_comision.empresa_id", "fuerza_ventas.cat_modalidades_pago_comision.id"],
            ondelete="RESTRICT",
            name="fk_mae_promotores_modalidad_comision",
        ),
        Index(
            "ix_mae_promotores_empresa_persona",
            "empresa_id",
            "persona_id",
        ),
        Index(
            "ix_mae_promotores_empresa_modalidad",
            "empresa_id",
            "modalidad_pago_comision_id",
        ),
        Index(
            "ix_mae_promotores_empresa_tipo",
            "empresa_id",
            "tipo_promotor",
        ),
        Index(
            "ix_mae_promotores_empresa_activo",
            "empresa_id",
            "is_activo",
        ),
        {"schema": "fuerza_ventas"},
    )

    # Identificadores y Claves
    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)",
    )

    usuario_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Correlación con core.identidad.mae_usuarios (sin FK cross-DB)",
    )

    persona_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Correlación con core.identidad.mae_personas (sin FK cross-DB)",
    )

    codigo_promotor: Mapped[str] = mapped_column(
        String(30),
        nullable=False,
        comment="Código interno comercial",
    )

    tipo_promotor_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Tipo o rol de promotor comercial (FK cat_tipos_promotor)",
    )

    modalidad_pago_comision_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Identificador de la modalidad de pago de comisión (FK cat_modalidades_pago_comision)",
    )

    is_coordinador: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        server_default=text("false"),
        comment="Indica si tiene rol de coordinación/supervisión",
    )

    is_casa_matriz: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        server_default=text("false"),
        comment="Indica si opera desde sede principal",
    )

    porcentaje_comision: Mapped[float] = mapped_column(
        Numeric(5, 2),
        nullable=False,
        server_default=text("0.00"),
        comment="Porcentaje base de comisión",
    )

    monto_cupo_maximo_credito: Mapped[float] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        server_default=text("0.00"),
        comment="Cupo máximo acumulado otorgable a su cartera",
    )

    dispositivo_imei: Mapped[Optional[str]] = mapped_column(
        String(50),
        nullable=True,
        comment="Identificador del dispositivo móvil autorizado",
    )

    telefono_corporativo: Mapped[Optional[str]] = mapped_column(
        String(50),
        nullable=True,
        comment="Número móvil institucional",
    )

    # Auditoría General
    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()"),
        onupdate=datetime.utcnow,
        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",
    )

```python """create mae_promotores

Revision ID: mae_0002 Revises: mae_0001 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 = "mae_0002" down_revision: Union[str, Sequence[str], None] = "mae_0001" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None

def upgrade() -> None:

op.create_table(
    "mae_promotores",

    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(
        "usuario_id",
        postgresql.UUID(as_uuid=True),
        nullable=False,
        comment="Correlación con core.identidad.mae_usuarios (sin FK cross-DB)",
    ),

    sa.Column(
        "persona_id",
        postgresql.UUID(as_uuid=True),
        nullable=False,
        comment="Correlación con core.identidad.mae_personas (sin FK cross-DB)",
    ),

    sa.Column(
        "codigo_promotor",
        sa.String(length=30),
        nullable=False,
        comment="Código interno comercial",
    ),

    sa.Column(
        "tipo_promotor_id",
        postgresql.UUID(as_uuid=True),
        nullable=False,
        comment="Tipo o rol de promotor comercial (FK cat_tipos_promotor)",
    ),

    sa.Column(
        "modalidad_pago_comision_id",
        postgresql.UUID(as_uuid=True),
        nullable=True,
        comment="Identificador de la modalidad de pago de comisión (FK cat_modalidades_pago_comision)",
    ),

    sa.Column(
        "is_coordinador",
        sa.Boolean(),
        nullable=False,
        server_default=sa.text("false"),
        comment="Indica si tiene rol de coordinación/supervisión",
    ),

    sa.Column(
        "is_casa_matriz",
        sa.Boolean(),
        nullable=False,
        server_default=sa.text("false"),
        comment="Indica si opera desde sede principal",
    ),

    sa.Column(
        "porcentaje_comision",
        sa.Numeric(precision=5, scale=2),
        nullable=False,
        server_default=sa.text("0.00"),
        comment="Porcentaje base de comisión",
    ),

    sa.Column(
        "monto_cupo_maximo_credito",
        sa.Numeric(precision=12, scale=2),
        nullable=False,
        server_default=sa.text("0.00"),
        comment="Cupo máximo acumulado otorgable a su cartera",
    ),

    sa.Column(
        "dispositivo_imei",
        sa.String(length=50),
        nullable=True,
        comment="Identificador del dispositivo móvil autorizado",
    ),

    sa.Column(
        "telefono_corporativo",
        sa.String(length=50),
        nullable=True,
        comment="Número móvil institucional",
    ),

    # 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_mae_promotores",
    ),

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

    sa.UniqueConstraint(
        "empresa_id",
        "codigo_promotor",
        name="uq_mae_promotores_empresa_codigo",
    ),

    sa.UniqueConstraint(
        "empresa_id",
        "usuario_id",
        name="uq_mae_promotores_empresa_usuario",
    ),

    sa.ForeignKeyConstraint(
        ["empresa_id", "modalidad_pago_comision_id"],
        ["fuerza_ventas.cat_modalidades_pago_comision.empresa_id", "fuerza_ventas.cat_modalidades_pago_comision.id"],
        ondelete="RESTRICT",
        name="fk_mae_promotores_modalidad_comision",
    ),

    comment="Maestro de Promotores y Asesores Comerciales",
    schema="fuerza_ventas",
)

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

op.create_index(
    "ix_mae_promotores_empresa_modalidad",
    "mae_promotores",
    ["empresa_id", "modalidad_pago_comision_id"],
    unique=False,
    schema="fuerza_ventas",
)

op.create_index(
    "ix_mae_promotores_empresa_tipo",
    "mae_promotores",
    ["empresa_id", "tipo_promotor"],
    unique=False,
    schema="fuerza_ventas",
)

op.create_index(
    "ix_mae_promotores_empresa_persona",
    "mae_promotores",
    ["empresa_id", "persona_id"],
    unique=False,
    schema="fuerza_ventas",
)

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

op.execute(
    """
    CREATE POLICY rls_mae_promotores_empresa
    ON fuerza_ventas.mae_promotores
    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_mae_promotores_empresa
    ON fuerza_ventas.mae_promotores;
    """
)

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

op.drop_index(
    "ix_mae_promotores_empresa_persona",
    table_name="mae_promotores",
    schema="fuerza_ventas",
)

op.drop_index(
    "ix_mae_promotores_empresa_tipo",
    table_name="mae_promotores",
    schema="fuerza_ventas",
)

op.drop_index(
    "ix_mae_promotores_empresa_modalidad",
    table_name="mae_promotores",
    schema="fuerza_ventas",
)

op.drop_index(
    "ix_mae_promotores_empresa_activo",
    table_name="mae_promotores",
    schema="fuerza_ventas",
)

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