Cosmos.Impuestos.Contratos 0.33.0-beta

This is a prerelease version of Cosmos.Impuestos.Contratos.
dotnet add package Cosmos.Impuestos.Contratos --version 0.33.0-beta
                    
NuGet\Install-Package Cosmos.Impuestos.Contratos -Version 0.33.0-beta
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Cosmos.Impuestos.Contratos" Version="0.33.0-beta" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Cosmos.Impuestos.Contratos" Version="0.33.0-beta" />
                    
Directory.Packages.props
<PackageReference Include="Cosmos.Impuestos.Contratos" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Cosmos.Impuestos.Contratos --version 0.33.0-beta
                    
#r "nuget: Cosmos.Impuestos.Contratos, 0.33.0-beta"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Cosmos.Impuestos.Contratos@0.33.0-beta
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Cosmos.Impuestos.Contratos&version=0.33.0-beta&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Cosmos.Impuestos.Contratos&version=0.33.0-beta&prerelease
                    
Install as a Cake Tool

Cosmos.Impuestos.Contratos

Eventos públicos de integración de Cosmos Impuestos (IPublicEvent). Un consumidor en otro bounded context referencia este paquete y publica/consume estos eventos sobre el bus para integrarse con Impuestos de forma eventual (async request-reply, event-carried state transfer y avisos de cambio).

El paquete distribuye los tipos C# de los contratos. No incluye stubs gRPC: la integración es por eventos sobre el bus de mensajería, no por RPC síncrono.


Instalación

<PackageReference Include="Cosmos.Impuestos.Contratos" Version="0.33.0-beta" />

Depende de Cosmos.EventDriven.Abstractions (IPublicEvent), Cosmos.Types y Cosmos.Types.IdentificacionesLegales.

Cambios incompatibles

Las versiones se listan de la más nueva a la más vieja. Una entrada está acá cuando obliga a shippear código para seguir siendo correcto, compile o no: una remoción, un cambio de forma, o un valor nuevo en un enum sobre el que se discrimina. Por ese último caso una entrada puede ser incompatible sin cambiar ninguna firma — ver «Miembros de enum agregados» al final.

0.33.0-beta — un rechazo por configuración contradictoria o por un dato que el tributo exige dice quién lo corrige

Varios rechazos del cálculo salían como Indeterminado, que no dice de quién es el defecto. Incompatible sin cambiar ninguna firma: un valor nuevo en MotivoRechazoCalculo rompe a quien haga switch exhaustivo, quien discriminaba por Indeterminado ya no recibe estos casos, y quien discriminaba DatosFaltantes en ConfirmacionTributariaRechazada recibe ComandoInvalido en dos de ellos; además, quien mande conceptos con el mismo id ve su solicitud rechazada.

  • MotivoRechazoCalculo.ConfiguracionIncoherente — al final del enum. La configuración vigente existe pero se contradice o deja sin resolver qué debe pasar (dos condiciones que coinciden sin llevar al mismo resultado, una sustitución cíclica, …). La corrige quien configura: no invita a reintentar sin corregirla.
  • Casos que pasan de Indeterminado a un motivo que dice quién corrige:
    • a ConfiguracionIncoherente, las configuraciones contradictorias;
    • a ComandoInvalido, un dato que el contrato permite omitir pero que el tributo exige (el lugar de ejecución de un tributo localizado, el concepto de pago de una tarifa por concepto de pago);
    • a ConfiguracionTributariaNoEncontrada, la falta del valor de referencia de una unidad (UVT, …) a la fecha de la transacción.
  • Revierte el criterio de 0.29.0-beta para esos dos casos. Aquella versión dejó en Indeterminado lo que depende de la configuración del tenant, para no atribuirle a quien solicita algo que puede no ser suyo. El lugar de ejecución y el concepto de pago son datos que el contrato permite omitir y que solo quien arma la solicitud puede aportar, así que ahora el motivo se lo atribuye.
  • Una solicitud con ids de concepto repetidos se rechaza con ComandoInvalido. Antes se calculaba; ahora el cálculo y la confirmación de un gravamen la rechazan. Cada concepto debe identificarse una sola vez; el detalle nombra el id repetido.
  • La confirmación reclasifica dos de esos casos. Un rechazo del cálculo llega a ConfirmacionTributariaRechazada con uno de dos motivos: ComandoInvalido cuando el defecto es de la solicitud y DatosFaltantes para el resto, incluida la configuración incoherente; los demás motivos de la confirmación no cambian. Sin lugar de ejecución y sin concepto de pago pasan de DatosFaltantes a ComandoInvalido. No cambia ninguna firma. Qué hacer: quien discriminaba Indeterminado revisa estos casos; quien trataba DatosFaltantes de la confirmación como «faltó un dato que corrijo y reintento» trata ComandoInvalido igual; quien mandaba conceptos con el mismo id les da ids distintos.

0.31.0-beta — una jurisdicción puede tarifar por línea y declarar actividades no sujetas

En una jurisdicción con catálogo de actividades, el factor del tributo por actividad ya no es la actividad sino la línea tarifaria que el catálogo le asigna, y el catálogo también puede declarar una actividad no sujeta. Incompatible: un valor nuevo en MotivoExclusion rompe a quien haga switch exhaustivo; el campo nuevo es opcional.

public record LineaDesglose(/* … */, Guid ConceptoOrigen, string? ActividadEconomica = null);
public record LineaConfirmada(/* … */, Guid ConceptoOrigen, string? ActividadEconomica = null);
public record LineaDescartada(/* … */, IReadOnlyList<string>? ActividadesCandidatas = null,
    string? ActividadEconomica = null);

LineaDesgloseExplicada y LineaDescartadaExplicada (las lecturas) llevan el mismo campo, al final.

  • MotivoExclusion.ActividadNoSujeta — al final del enum. La jurisdicción reconoce la actividad del sujeto pasivo y la declara no sujeta: el tributo por actividad se descarta, sin tarifa que liquidar. No se corrige cambiando la solicitud: es lo que la jurisdicción dispone para esa actividad.
  • ActividadEconomica en las cinco líneas — la actividad por la que se tarifó cuando el factor es de actividad. null en las líneas de otros factores. Qué hacer: con catálogo, FactorUtilizado es la línea tarifaria (la llave de la tarifa) y de ella no se reconstruye la actividad: para saber cuál fue, leé ActividadEconomica.
  • La confirmación la devuelve tal cual. La comparación contra el cálculo de referencia incluye la actividad: una línea que la traía y se confirma sin ella se registra como intervención.
  • Sin catálogo de la jurisdicción FactorUtilizado sigue siendo la actividad, y ActividadEconomica la repite: toda línea con factor de actividad la trae, haya o no catálogo.
  • Para elegir entre las variantes con que una jurisdicción parte una clase, la consulta de actividades vigentes acepta jurisdiccion y devuelve variantes (codigo, nombre) en cada actividad; sin ella la respuesta es la de siempre.

0.29.0-beta — la solicitud que no cumple el contrato tiene su motivo

Una solicitud mal armada se rechazaba con motivos que no decían de quién era el defecto: unos casos salían como Indeterminado y otros como DatoInvalido, y la confirmación los mezclaba con DatosFaltantes. Incompatible sin cambiar ninguna firma: un valor nuevo en MotivoRechazoCalculo rompe a quien haga switch exhaustivo, y quien discriminaba por DatoInvalido (la actividad informada vacía) o por DatosFaltantes en la confirmación de una solicitud mal armada ya no lo recibe.

  • MotivoRechazoCalculo.ComandoInvalido — al final del enum. La solicitud no cumple el contrato: un dato obligatorio ausente, vacío o con una forma que el contrato no admite. El defecto está en quien arma la solicitud, así que no invita a reintentar sin corregirla. Lo producen siete causas:

    • del concepto: el identificador vacío, la clasificación tributaria ausente o vacía, y la actividad económica informada vacía;
    • de la solicitud: la identificación emisora ausente, la de la contraparte ausente, la fecha de la transacción por defecto y la lista de conceptos vacía en un gravamen (la propuesta de un desgravamen sin conceptos devueltos sigue rechazándose con DatosFaltantes).

    El detalle nombra el concepto cuando el dato lo permite. Los casos que dependen de la configuración del tenant siguen en Indeterminado: no se le atribuye a quien solicita algo que puede no ser suyo.

  • DatoInvalido queda sin productor. Se conserva porque su ordinal está publicado. Qué hacer: quien lo discriminaba para la actividad informada vacía pasa a ComandoInvalido.

  • ConfirmacionTributariaRechazada reclasifica esos casos. Cuando el motor rechaza la solicitud de confirmación por estar mal armada, el motivo publicado pasa de DatosFaltantes a ComandoInvalido (MotivoRechazoConfirmacion.ComandoInvalido ya existía). Las demás fallas del motor siguen en DatosFaltantes. No cambia ninguna firma ni TransaccionYaConfirmada. Qué hacer: si tratabas DatosFaltantes de la confirmación como «faltó un dato que corrijo y reintento», tratá ComandoInvalido igual.

0.28.0-beta — MotivoRechazoCalculo gana DatosFaltantes; la confirmación de un desgravamen rechaza fuera del margen

Dos cambios de comportamiento en el flujo de desgravamen y uno en la medida de la intervención. Incompatible sin cambiar ninguna firma: un valor nuevo en MotivoRechazoCalculo rompe a quien haga switch exhaustivo, y quien discriminaba por DatoInvalido el caso de la propuesta sin conceptos ya no lo recibe.

  • MotivoRechazoCalculo.DatosFaltantes — al final del enum. La solicitud de cálculo de un desgravamen (OrigenDelDesgravamen) sin ningún concepto devuelto se rechaza con este motivo en vez de DatoInvalido: sin montos no hay prorrateo que proponer. Qué hacer: quien discrimine DatoInvalido para ese caso debe pasar a DatosFaltantes. DatoInvalido se conserva y sigue emitiéndose para otras formas inválidas de la solicitud (hoy, una actividad económica informada vacía).
  • ConfirmacionTributariaRechazada empieza a llevar IntervencionExcedeMargen — un miembro que ya existía en MotivoRechazoConfirmacion. Se emite cuando el DesgloseConfirmado de un desgravamen se aparta de su prorrateo en más de una unidad del último decimal de la escala del país, en el valor o en la base gravable, o en cualquier diferencia de tarifa, tipo, factor o línea (una línea faltante o sobrante cuenta). El desglose de una devolución deriva del gravamen de origen, así que solo puede diferir por redondeo. Qué hacer: confirmar el desglose que propuso el cálculo, sin modificarlo.
  • HuboIntervencion del registro tributario — en un gravamen, una diferencia de hasta esa unidad entre lo confirmado y el cálculo de referencia deja de marcar HuboIntervencion: es redondeo, no intervención. Más allá de la unidad sigue marcándose. El campo se consulta por HTTP; no viaja en el evento.

0.27.0-beta — el concepto informa la actividad económica y el motor no desempata

Un sujeto pasivo con varias actividades económicas generales dejaba la tarifa de un tributo por actividad al orden en que se registraron. Desde esta versión la actividad que corresponde a cada concepto la informa quien registra la transacción, y si no la informa y hay más de una candidata, el motor no elige. Incompatible: un valor nuevo en MotivoExclusion rompe a quien haga switch exhaustivo; los campos nuevos son opcionales.

public record ConceptoCalculo(Guid Id, string ClasificacionTributaria, decimal Monto, string? ConceptoPago,
    string? ActividadEconomica = null);

public record LineaDescartada(Tributo Tributo, decimal BaseGravable, decimal Tarifa, TipoTarifa TipoTarifa,
    decimal Valor, string? FactorUtilizado, Guid ConceptoOrigen, MotivoExclusion MotivoExclusion,
    string? ActividadRechazada = null, IReadOnlyList<string>? ActividadesCandidatas = null);
  • ConceptoCalculo.ActividadEconomica — el código de la actividad del sujeto pasivo que corresponde al concepto, en la solicitud de cálculo y en la de confirmación. null es «no la informo». Vale si el perfil del sujeto pasivo la tiene vigente a la fecha, general o inscrita en cualquier jurisdicción. Vacía o solo con espacios, el cálculo se rechaza con DatoInvalido y la confirmación con DatosFaltantes.
  • MotivoExclusion.ActividadAmbigua — el sujeto pasivo tiene varias actividades candidatas (las inscritas en la jurisdicción del tributo o, si no hay, las generales) y el concepto no informó cuál. La línea trae ActividadesCandidatas en orden ordinal. Qué hacer: indicar una de ellas en ActividadEconomica del concepto y recalcular. La confirmación no se rechaza por este motivo: se registra con el tributo descartado.
  • LineaDescartada.ActividadRechazada — con ActividadNoRegistrada, la actividad que el concepto informó y el perfil no tenía vigente. null si no se informó ninguna.
  • Para saber qué actividades tiene vigentes el sujeto pasivo a la fecha del documento: GET /perfiles-tributarios/{pais}/{tipoDocumento}/{numero}/actividades-economicas?fecha=yyyy-MM-dd (la fecha es obligatoria).

0.26.0-beta — los eventos de salida publican solo lo que un proceso usa al recibirlos

Los eventos de salida del cálculo, la confirmación y la entrega viajaban con el registro completo, y casi nada de eso lo usa quien los recibe: pesaba de más y obligaba a mantener en el bus lo que ya se consulta. Desde esta versión el bus lleva lo que se necesita para reconocer y tratar el resultado, y el resto se consulta. Rompe fuente; en el cable es compatible para quien solo lee lo que se conserva (ver abajo).

Qué sale de cada evento

Evento Sale Queda
CalculoTributarioRealizado ExplicacionDelCalculo; en cada LineaDesglose, RazonDeAplicacion y Explicacion; en cada LineaDescartada, Explicacion ReferenciaOrigen, Aplicados, Descartados, Correlacion
ConfirmacionTributariaRealizada Contexto, EntidadFiscalEmisora, EntidadFiscalContraparte, Jurisdiccion, HuboIntervencion, CalculoDeReferencia, LineasDescartadas, Periodo, Moneda, ConfiguracionUsada, y los montos de cada línea ReferenciaOrigen, RegistroTributarioId, DesgloseConfirmado como lista de LineaEtiquetada
RegistroTributarioEntregado los mismos campos, y los de cada línea que no son su tributo ni sus montos ReferenciaSolicitud, RegistroTributarioId, DesgloseConfirmado como lista de LineaEntregada

Tributo pierde Pais, Familia y MecanismoDeRecaudo. El país lo da el tenant; la familia y el mecanismo de recaudo se consultan. Al salir Pais, Codigo pasa a ser el primer parámetro posicional:

public record Tributo(
    string Codigo,
    string Nombre,
    Naturaleza Naturaleza,
    CaracterRetencion CaracterRetencion,
    TributoDeColombia? DeColombia);

public record LineaEtiquetada(Tributo Tributo, Guid ConceptoOrigen);

public record LineaEntregada(Tributo Tributo, decimal BaseGravable, decimal Tarifa, TipoTarifa TipoTarifa,
    decimal Valor);

LineaDesglose y LineaDescartada conservan sus montos, FactorUtilizado y ConceptoOrigen, y solo pierden la prosa. LineaConfirmada, la línea de la solicitud de confirmación, no cambia, ni tampoco ninguna solicitud entrante.

Tipos nuevos: LineaEtiquetada y LineaEntregada, en Compartidos. En el namespace Cosmos.Impuestos.Contratos.Lecturas —lo que devuelven las consultas HTTP, no el bus— TributoDeLectura, LineaDesgloseExplicada, LineaDescartadaExplicada y ResultadoDelCalculoExplicado: un consumidor del bus no los necesita.

Tipos borrados, porque fuera de estos eventos nada los usa: EntidadFiscal, AtributoSnapshot, Jurisdiccion, ConfiguracionUsada, TarifaUsada, CondicionEvaluada, IndiceDeReferenciaUsado, ConvenioUsado y CuantiaMinima (en Compartidos), y los enums UnidadDeReferencia y ResultadoEvaluacion. Las entradas de más abajo los nombran tal como eran en su versión.

En el cable. Cada campo que se conserva mantiene su nombre y su ruta (tributo.codigo, conceptoOrigen, baseGravable…), así que un consumidor con el paquete anterior lee lo mismo que leía. Lo que se retiró llega sin aviso del deserializador, y de dos maneras según el tipo del campo: los valores (montos, HuboIntervencion, Moneda) llegan en cero, en false o en el primer miembro del enum, o sea un dato falso que parece válido; los objetos (Contexto, las entidades fiscales, Jurisdiccion, ConfiguracionUsada) llegan nulos y fallan al leerse, ya en ejecución. En ningún caso el compilador avisa antes de actualizar el paquete: conviene actualizar antes de depender de esos campos.

Dónde está ahora lo que salió. Se consulta, y las dos consultas HTTP no cambian de forma:

  • La prosa del cálculo y la de cada línea: GET /explicaciones-de-cotizacion/{referenciaOrigen}?correlacion={guid}.
  • El registro confirmado completo —contexto, entidades, jurisdicción, período, moneda, cálculo de referencia, descartadas y procedencia de la configuración—: GET /registros-tributarios/{subDominio}/{transaccionId}. SolicitarRegistroTributario entrega solo el desglose confirmado.

Otros efectos observables

  • La entrega solo rechaza con RegistroInexpresable por un valor de las líneas del desglose confirmado que el contrato no pueda llevar. Ya no rechaza por moneda, dirección fiscal o efecto de condición: esos campos no viajan, y un registro con líneas sanas se entrega.
  • La transacción de origen ya no viaja en la entrega (antes en Contexto.TransaccionId): quien la pide ya la conoce, y el registro se identifica por RegistroTributarioId y ReferenciaSolicitud.
  • La confirmación ya no devuelve los montos: los mandó el consumidor. Devuelve el Tributo que Impuestos definió para cada línea y su concepto de origen, que es lo que sirve para emparejar.

0.25.0-beta — la identidad del tributo se agrupa en un objeto Tributo

La forma vigente de estos tipos está en 0.26.0-beta, arriba. LineaDesglose y LineaDescartada (en Compartidos) dejan de llevar la identidad del tributo como campos sueltos y pasan a agruparla en un único objeto:

public record Tributo(
    Pais Pais,
    string Codigo,
    string Nombre,
    FamiliaTributaria? Familia,
    Naturaleza Naturaleza,
    CaracterRetencion CaracterRetencion,
    MecanismoDeRecaudo? MecanismoDeRecaudo,
    TributoDeColombia? DeColombia);

public record LineaDesglose(
    Tributo Tributo, decimal BaseGravable, decimal Tarifa, TipoTarifa TipoTarifa, decimal Valor,
    string? FactorUtilizado, Guid ConceptoOrigen,
    RazonDeAplicacion? RazonDeAplicacion = null, Explicacion? Explicacion = null);

public record LineaDescartada(
    Tributo Tributo, decimal BaseGravable, decimal Tarifa, TipoTarifa TipoTarifa, decimal Valor,
    string? FactorUtilizado, Guid ConceptoOrigen, MotivoExclusion MotivoExclusion,
    Explicacion? Explicacion = null);

Se van los campos sueltos CodigoTributo, NombreTributo, Naturaleza, CaracterRetencion, MecanismoDeRecaudo, Familia y TributoDeColombia (este último se llama DeColombia dentro de Tributo, mismo enum). Tributo es el primer parámetro posicional de los dos records, así que rompe fuente a quien construya una LineaDesglose/LineaDescartada, y cambia la forma serializada para quien lea por clave (codigoTributo → tributo.codigo, y así con el resto).

  • Pais (Cosmos.Types.Paises.Pais) es nuevo: antes ninguna de las dos líneas lo llevaba. Se congela en el registro al momento del cálculo, desde el país de la configuración con que se calculó.

  • Fuera de Colombia la identidad del tributo es (Pais, Codigo), sin un campo por país: un campo TributoDe<País> por país sería una unión discriminada disfrazada de N nulables mutuamente excluyentes.

  • DeColombia conserva el mismo criterio de derivación por código que tenía TributoDeColombia antes de este cambio — ver «Riesgo aceptado» en «DeColombia en Tributo» abajo.

  • Absorbe #362: LineaDescartada nunca había llevado Familia ni el tributo de Colombia — solo la aplicada los tenía. Con el objeto compartido, el descarte los trae sin trabajo aparte.

  • La entrada de confirmación es una línea angosta: SolicitarConfirmacionTributaria.DesgloseConfirmado pasa a ser una lista de LineaConfirmada, que nombra el tributo solo por código:

    public record LineaConfirmada(string CodigoTributo, decimal BaseGravable, decimal Tarifa,
        TipoTarifa TipoTarifa, decimal Valor, string? FactorUtilizado, Guid ConceptoOrigen);
    

    El resto del Tributo lo define Impuestos y la ConfirmacionTributariaRealizada lo devuelve completo. En un gravamen sale del cálculo de referencia para ese tributo y ese concepto; si no está, del descarte que el consumidor reincorpora; y si tampoco, del catálogo de tributos vigente del tenant, lo que cuenta como intervención manual (huboIntervencion = true). Un código que no está en ninguno de los tres se rechaza con ComandoInvalido. En un desgravamen sale del registro origen, y un código que el origen no tiene sigue rechazándose con ConceptoNoExisteEnOrigen.

  • Corte directo, sin período de convivencia: no hay una versión intermedia con los campos sueltos deprecados conviviendo con Tributo.

0.24.0-beta — un pedido de cálculo puede ser un desgravamen; confirmar uno cambia de contrato

  • El camino de cálculo es aditivo; el de confirmación de un desgravamen NO lo es. Un solicitante de CalculoTributarioSolicitado que no envía OrigenDelDesgravamen no ve ningún cambio. Pero quien ya confirma desgravámenes con SolicitarConfirmacionTributaria sí tiene que shippear código, sin tocar ningún campo nuevo: es incompatible por la misma regla de arriba (obliga a shippear código para seguir siendo correcto, compile o no), y el precedente es 0.23.0-beta: lo que era ConfirmacionTributariaRealizada pasa a ser ConfirmacionTributariaRechazada en tres casos nuevos. Concretamente:
    • Conceptos pasa a leerse, y no puede llegar vacío. Antes la referencia se derivaba prorrateando el DesgloseConfirmado y Conceptos no se leía. Ahora, si Conceptos viene vacío, la confirmación se rechaza con MotivoRechazoConfirmacion.DatosFaltantes. Cubre también el concepto individual: todo concepto que venga en DesgloseConfirmado tiene que venir en Conceptos, porque su monto devuelto es lo que se descuenta del saldo de la transacción origen; si falta, la confirmación se rechaza con DatosFaltantes.
    • DesgravamenExcedeSaldo gana productor. Una devolución que, sumada a las anteriores del mismo concepto, supera lo que ese concepto se gravó en la transacción origen —antes se confirmaba igual— ahora se rechaza.
    • ConceptoNoExisteEnOrigen también sale de Conceptos, además de DesgloseConfirmado.
    • La referencia contra la que se compara lo confirmado cambia: se deriva de Conceptos con cierre por residuo, no del DesgloseConfirmado sin más.
  • CalculoTributarioSolicitado gana OrigenDelDesgravamen ({ SubDominio, TransaccionId }), opcional y al final — esta parte sí es aditiva. Si viene, la solicitud es la cotización de un desgravamen sobre esa transacción: Impuestos no ejecuta el motor, busca el RegistroTributario de la transacción de origen y deriva el desglose por prorrateo (un factor por concepto, R((valor × devuelto) ÷ gravado), hijos porcentajeDePadre incluidos, con el redondeo del país). Sin el campo, el flujo es el mismo de siempre (gravamen, motor). Una solicitud de desgravamen sin ningún concepto devuelto se rechaza con MotivoRechazoCalculo.DatoInvalido.
  • MotivoRechazoCalculo gana OrigenNoEncontrado, ConceptoNoExisteEnOrigen y DesgravamenExcedeSaldo, al final — ver «Miembros de enum agregados» abajo. Estos tres sí son aditivos: MotivoRechazoCalculo es el enum que crece sin coordinación (ver la nota antes de la tabla).
  • Cada desgravamen confirmado descuenta de un saldo por transacción de origen. El saldo sirve tanto a una compra como a una venta desgravada (el registro no distingue dirección). Proponer o confirmar un monto que, sumado a lo ya devuelto de un concepto, supere lo que ese concepto se gravó se rechaza (DesgravamenExcedeSaldo en el cálculo y en la confirmación) en vez de aceptarse — la validación es por monto del concepto, no por tributo dentro del concepto. La última devolución que completa un concepto sigue pasando por esa misma guarda; lo que cambia es que la referencia revierte, para cada tributo del concepto, el valor exacto que le queda por revertir (residuo) en vez del prorrateo redondeado, así que devoluciones sucesivas cuadran al centavo.

0.23.0-beta — una clasificación que no rige rechaza el cálculo, y los códigos vuelven a ser los del libro

  • Los códigos que viajan son los de erp-definiciones, tal cual. Se retira la homologación a códigos internos de 0.20.0-beta: clasificaciones, condiciones, regímenes y definiciones de calidad llegan en las trazas del cálculo y en las lecturas con el código que publica el libro (GRAV_19, perteneceRegimenIVA, RTF-01a, ZF-BAQ), que es también el que se envía al pedir un cálculo o escribir un perfil. Quien guardó o compara códigos internos tiene que volver a los del libro; no hay tabla de traducción en ningún sentido, y un nombre de calidad que no está en el catálogo vigente del país se rechaza al escribir el perfil.
  • MotivoRechazoCalculo gana ClasificacionNoVigente, al final. Un concepto cuya clasificación tributaria no existe en la configuración del país, o no está vigente a la fecha de la transacción, ya no produce un desglose en cero con todos los tributos descartados por ClasificacionExcluida: el cálculo se rechaza con este motivo, y el detalle nombra el código, el país y la fecha. Quien hoy recibe ese cero por un código mal escrito o de otro país empieza a recibir CalculoTributarioRechazado. Lo exige el libro de definiciones (R32 e I26).

0.22.0-beta — FechaTransaccion es un día

FechaTransaccion pasa de DateTimeOffset a DateOnly en CalculoTributarioSolicitado y en ContextoTransaccional, que viaja en SolicitarConfirmacionTributaria, RegistroTributarioEntregado y ConfirmacionTributariaRealizada.

  • En el cable es "2025-12-31" (yyyy-MM-dd). Un mensaje que siga mandando la fecha con hora ("2025-12-31T00:00:00-05:00") no deserializa: la solicitud no llega al cálculo y no hay respuesta, ni CalculoTributarioRechazado para un cálculo ni ConfirmacionTributariaRechazada para una confirmación. No hay versión intermedia que acepte los dos formatos, así que quien produce estos eventos tiene que actualizar el paquete y mandar el día de la transacción.
  • Por qué. El modelo de dominio de Impuestos en erp-definiciones (dominio/impuestos/modelo-dominio.md) tipa la fecha de la transacción como date, y para calcular solo importa el día: con él se eligen la configuración vigente y los índices de referencia (la UVT de ese año). Con DateTimeOffset el día se leía con el reloj del offset recibido, así que el mismo instante mandado desde otra zona podía caer en otro día, y el 31 de diciembre en otro año de UVT.
  • En la salida el día publicado es el que el cálculo usó, también en los registros confirmados antes de esta versión. Quien solo consume y sigue con el paquete anterior no se rompe: "2025-12-31" deserializa en su DateTimeOffset como la medianoche de ese día con el offset local de su máquina.
  • PerfilTributarioActualizado.FechaDelCambio sigue siendo DateTimeOffset: es cuándo quedó persistido un cambio, un instante y no un día.

Para el front (API HTTP, fuera de este paquete): GET /registros-tributarios/{subDominio}/{transaccionId} sirve contexto.fechaTransaccion como día, "2025-12-31", en vez de un instante con offset. Es el día con que se calculó, también en los registros anteriores. Se muestra como día, sin hora: dayjs(valor) lo lee como la medianoche local y conserva el día, pero new Date(valor) lo lee como medianoche UTC y en Colombia lo muestra el día anterior.

0.20.0-beta — la configuración estándar sale de ediciones publicadas

El estándar fiscal de cada país deja de ser una siembra y pasa a ser una edición inmutable, con número y huella, construida desde el contenido que publica el equipo fiscal. Lo que cambia para quien consume:

  • Códigos internos (revertido en 0.23.0-beta). Los códigos de clasificaciones, condiciones, regímenes, definiciones y calidades fiscales que viajan en los eventos (las trazas del cálculo) son los internos de la tabla de homologación, en mayúsculas con guion bajo en vez del código del libro (GRAV_19, regimenTributario). Quien guarde o compare códigos tiene que migrarlos con esa tabla, que vive en el repositorio de Impuestos, un archivo por catálogo y país: contenido/declarado/ (sección homologacion: código de la fuente → código interno). Los cálculos y perfiles anteriores conservan el nombre de la fuente y se siguen interpretando.
  • Vigencias semiabiertas. Toda vigencia es [desde, hasta): hasta es el primer día en que deja de regir. Un tramo con hasta = desde nunca rigió.
  • Se retira la réplica de clasificaciones por el bus: ClasificacionTributariaPublicada, ClasificacionTributariaDesactivada y ReplicaDeClasificacionesSolicitada, con sus topics. Ningún servicio la consumía: las clasificaciones de un país se leen por HTTP en la API de consultas. Las suscripciones que el Terraform de otros repos crea sobre esos topics quedan sin publicador y se pueden borrar.
  • Configuracion se conserva (Estandar / Personalizado).

Para el front (API HTTP, fuera de este paquete; detalle en el ADR-004 «Contrato afectado»): las excepciones se registran con los endpoints nuevos (decisiones de tratamiento con vigencia, suprimir y cambiar el fin de una tarifa, convenios), la cabecera de un tributo o una clasificación de la edición no se edita (422), desactivar acepta desde, las lecturas traen todos los tramos y marcan las supresiones, y la bandeja de revisión se consulta en GET /bandeja-de-revision/{pais}.

0.19.0-beta — MotivoExclusion gana TarifaNoConfigurada y ConvenioExime

Dos motivos de descarte nuevos, al final del enum, así que los existentes conservan su ordinal. Viajan en LineaDescartada.MotivoExclusion y en CondicionEvaluada.MotivoDescarte. Qué significa cada uno está en la tabla de «Miembros de enum agregados».

Es incompatible aunque no cambie ninguna firma: quien muestra un descarte lo rotula según su motivo, y un motivo que no reconoce cae al rótulo por defecto sin error. El descarte sigue trayendo su Explicacion, que sí nombra la causa; lo que se pierde es el rótulo corto.

En la misma versión, aditivo: TarifaUsada porta ConvenioUsado? Convenio, nullable con default y al final. Viene poblado cuando la tarifa la fijó un convenio para evitar la doble imposición, con el país contraparte (código ISO 3166-1 alfa-2, porque puede ser un país donde el productor no opera), el nombre del convenio y su instrumento normativo. TarifaUsada viaja en la ConfiguracionUsada de RegistroTributarioEntregado y de ConfirmacionTributariaRealizada.

0.18.0-beta — TipoTarifa gana PorMil

Una tarifa cuyo valor se lee por mil. Viaja en TarifaUsada, LineaDesglose y LineaDescartada. Miembro nuevo al final del enum, así que Porcentaje y Especifica conservan su ordinal.

La unidad de la Tarifa depende del tipo, y con PorMil deja de ser una fracción:

TipoTarifa Qué es Tarifa Ejemplo
Porcentaje fracción decimal 0.19 es 19 %
Especifica monto absoluto en la moneda del registro 1900 son 1.900
PorMil el número en ‰, no una fracción 4.14 es 4,14 ‰

Rompe a quien haga switch exhaustivo sin arm por defecto y, más callado, a quien formatee la tarifa según su tipo: el tipo que no reconoce cae al caso por defecto y pierde la unidad sin error, y quien lo trate como Porcentaje (× 100) muestra 4.14 como 414 %. Es ese segundo caso —el que ninguna firma delata— el que la vuelve incompatible y no aditiva.

0.15.0-beta — cierre de la reestructuración a agregados por elemento

Tres cambios en la misma versión.

MotivoRechazoCalculo.AtributoFiscalFaltante → CalidadFiscalFaltante. Conserva su ordinal, así que el wire no cambia y quien deserialice por número no ve nada. Rompe fuente a quien nombre el miembro en código; se arregla con el rename.

ConfiguracionUsada pierde CatalogoId. Viaja en RegistroTributarioEntregado.ConfiguracionUsada y en ConfirmacionTributariaRealizada.ConfiguracionUsada, los dos únicos eventos que la portan; ningún evento de cálculo la lleva. El catálogo por país dejó de existir —la configuración tributaria pasó a un stream por elemento— así que el campo venía viajando Guid.Empty. Es el primer parámetro posicional del record, así que rompe fuente a quien construya una ConfiguracionUsada, y deja de serializar la clave catalogoId para quien la lea.

La misma remoción aplica al response de GET /registros-tributarios/{subDominio}/{transaccionId}, que no lleva versión semántica: ahí la clave desaparece sin más señal que esta nota. No hay sustituto porque no hay dato que sustituir. La procedencia de la configuración —qué stream de tributo, estándar o del tenant, produjo cada línea— no viaja por el bus: se obtiene por ese mismo GET, en configuracionUsada.tributosUsados, que llega nulo en los registros cuya traza no la congeló (los anteriores a la reestructuración).

ConfiguracionTributariaNoEncontrada vuelve a emitirse. Entre el despliegue de la reestructuración y esta versión, un cálculo contra un país sin tributos configurados no rechazaba: publicaba CalculoTributarioRealizado con el desglose vacío, o sea un cálculo que se veía exitoso y no llevaba nada. Vuelve a llegar CalculoTributarioRechazado con ese motivoCodigo y un motivoDetalle que nombra qué configuración falta. No agrega ningún miembro al enum: lo que cambia es que el valor vuelve a llegar. Rompe a quien haya adaptado su código a lo observado en esa ventana, tratando el desglose vacío como respuesta válida. Que el desglose quede vacío sigue siendo un desenlace legítimo cuando la configuración sí está y ningún tributo aplica: ése no rechaza.

0.14.0-beta — Naturaleza gana Provision

La tercera dirección de un tributo frente al valor de la operación. Viaja en LineaDesglose.Naturaleza y LineaDescartada.Naturaleza. Miembro nuevo al final del enum, así que los dos valores existentes conservan su ordinal.

Rompe a quien haga switch exhaustivo sin arm por defecto y, más callado, a quien particione un desglose en dos grupos por naturaleza: una partición binaria deja las provisiones afuera sin error, y es ese segundo caso —el que ninguna firma delata— el que la vuelve incompatible y no aditiva.

La configuración fiscal estándar todavía no declara ningún tributo como provisión, pero el alta y la modificación de tributo sí admiten la naturaleza nueva: una configuración propia puede emitirla en cuanto esta versión esté desplegada del lado del productor. Conviene adoptarla antes de eso, no después.

0.12.0-beta — Explicacion queda en las cinco partes del argumento

{ Efecto, Hechos, Regla, Contraste?, Advertencia? }. Se van Causa y Detalle, la prosa corrida que 0.11.0 conservó deprecada; quien quiera prosa encadena las partes que necesite. Y se va Autoridad, que nunca pudo ser no-nula: ninguna traza congelada porta la fuente normativa de la regla. El día que el catálogo la incorpore, Autoridad vuelve como campo nullable al final — aditivo para quien la lea, pero cambia la firma del constructor, así que quien construya una Explicacion recompila.

Contraste y Advertencia quedan sin valor por omisión: siguen siendo nullables, pero se pasan siempre. Rompe a quien lea Causa, Detalle o Autoridad, y a quien construya una Explicacion. Impuestos no persiste la explicación (la regenera on-read desde la traza congelada), así que no hay migración de este lado; un consumidor que la haya guardado encuentra las claves de más al deserializar.

0.11.0-beta — Explicacion pasa a forma estructurada

De (Causa, Detalle) a { Efecto, Hechos, Regla, Autoridad?, Contraste?, Advertencia? }: el resultado observable para el lector, los hechos que lo fundan, la norma que autoriza el paso de unos a otro, su fuente cuando la traza la conserva, el desenlace alternativo, y el alcance que la explicación no verificó.

Causa y Detalle se conservan deprecados y derivados de esa estructura, para que un consumidor que todavía no la renderice siga recibiendo texto coherente; se remueven en 0.12.0. Rompe a quien construya una Explicacion; no rompe a quien solo la lea.

0.7.0-beta — el país de los eventos públicos pasa a VO validado

Jurisdiccion.Pais (en ClasificacionTributariaPublicada / Desactivada) y ReplicaDeClasificacionesSolicitada.Pais pasan de enum a VO con wire objeto { "codigo": "CO" }. Cambia la forma serializada del campo país — coordinación cross-BC con los consumidores.

0.6.0-beta — la identidad fiscal pasa al tipo compartido IdentificacionLegal

La identificación del emisor y la contraparte deja de ser el modelo local Compartidos.IdentificacionFiscal (país como enum + número plano) y pasa a ser Cosmos.Types.IdentificacionesLegales.IdentificacionLegal (del paquete Cosmos.Types). Afecta a CalculoTributarioSolicitado, SolicitarConfirmacionTributaria y al modelo EntidadFiscal de las respuestas.

Cambia el wire (un consumidor de una versión previa se rompe hasta adoptar este formato):

  • El país viaja como objeto { "codigo": "CO" } (antes era un entero del enum).
  • El número se acompaña de tipo de documento y dígito de verificación, y se valida contra el catálogo del país (tipo país-scoped × número × DV). La igualdad de la identidad recae en (tipo, número, país); el dígito de verificación no participa.

El borde de Impuestos resuelve el perfil por la clave canónica {país}:{tipoDocumento}:{número}, así que el número que se envíe debe corresponder al del alta del perfil (el paquete normaliza el número al construir la identidad).

Miembros de enum agregados

Índice completo de las altas de miembro, que es la clase de cambio que no delata ninguna firma. Ningún alta cambia una firma y todas se agregan al final, así que los valores existentes conservan su ordinal y quien deserialice por número no cambia. Aun así rompen a quien haga switch exhaustivo sin arm por defecto: si ese arm lanza, el mensaje muere en vez de degradarse.

Por eso la columna «Etiqueta» no siempre dice lo mismo que el efecto. Una versión se etiqueta incompatible cuando el alta abre además un modo de rotura que ninguna firma delata —una partición o un formateo por tipo que deja el miembro nuevo afuera sin error—, y aditiva cuando el switch es el único riesgo, porque el consumidor no ramifica por ese enum — pero para un consumidor con switch exhaustivo las dos obligan a shippear código igual. Si tenés un arm por defecto que lanza, tratá las dos filas como incompatibles.

MotivoRechazoCalculo es el caso donde esto pasa más seguido: su conjunto es interno al productor para su observabilidad —el consumidor no ramifica por el código, solo lo registra o muestra el detalle—, así que un alta que no reclasifica nada crece sin coordinación, siempre agregando al final. El alta que saca casos de Indeterminado o de otro motivo (0.28.0-beta, 0.29.0-beta, 0.33.0-beta) se rotula incompatible: quien discriminaba el motivo anterior deja de recibirlos.

Versión Etiqueta Enum Miembro Qué significa
0.33.0-beta incompatible MotivoRechazoCalculo ConfiguracionIncoherente La configuración vigente existe pero se contradice o deja sin resolver qué debe pasar (dos condiciones que coinciden sin llevar al mismo resultado, una sustitución cíclica, …). La corrige quien configura: no invita a reintentar sin corregirla. Antes esos casos salían como Indeterminado. Ver 0.33.0-beta arriba.
0.31.0-beta incompatible MotivoExclusion ActividadNoSujeta La jurisdicción reconoce la actividad del sujeto pasivo y la declara no sujeta: el tributo por actividad se descarta sin tarifa que liquidar, y la línea trae la actividad en ActividadEconomica. Ver 0.31.0-beta arriba.
0.29.0-beta incompatible MotivoRechazoCalculo ComandoInvalido La solicitud no cumple el contrato: un dato obligatorio ausente, vacío o con una forma que el contrato no admite. El defecto está en quien arma la solicitud: no invita a reintentar sin corregirla. Antes esos casos salían como Indeterminado o DatoInvalido. Ver 0.29.0-beta arriba.
0.28.0-beta incompatible MotivoRechazoCalculo DatosFaltantes La solicitud de un desgravamen no trae ningún concepto devuelto: sin montos no hay prorrateo que proponer. Antes ese caso salía como DatoInvalido. Ver 0.28.0-beta arriba.
0.27.0-beta incompatible MotivoExclusion ActividadAmbigua El sujeto pasivo tiene varias actividades económicas candidatas y el concepto no informó cuál le corresponde: el tributo por actividad se descarta con las candidatas en ActividadesCandidatas, en orden ordinal. Se resuelve indicando una en ConceptoCalculo.ActividadEconomica y recalculando. Ver 0.27.0-beta arriba.
0.24.0-beta aditiva MotivoRechazoCalculo OrigenNoEncontrado La solicitud pide un desgravamen por prorrateo (OrigenDelDesgravamen) y no existe un registro tributario confirmado para la transacción de origen que indica.
0.24.0-beta aditiva MotivoRechazoCalculo ConceptoNoExisteEnOrigen Un concepto del desgravamen no se gravó en la transacción de origen, el registro no conserva su monto, o se gravó con monto 0 — en los tres casos falta el denominador del que derivar el prorrateo.
0.24.0-beta aditiva MotivoRechazoCalculo DesgravamenExcedeSaldo El desgravamen propuesto, sumado a lo ya devuelto de ese mismo concepto, superaría lo que ese concepto se gravó en la transacción origen (la validación es por monto del concepto, no por tributo dentro del concepto).
0.23.0-beta incompatible MotivoRechazoCalculo ClasificacionNoVigente Un concepto de la solicitud referencia una clasificación tributaria que no existe en la configuración del país o no está vigente a la fecha de la transacción. Antes el cálculo salía en cero, con todos los tributos descartados por ClasificacionExcluida, indistinguible de un concepto que de verdad no grava. Es incompatible aunque el consumidor no ramifique por el motivo: lo que era un CalculoTributarioRealizado pasa a ser un CalculoTributarioRechazado. El defecto está en la solicitud (el código): se corrige del lado del solicitante. Ver 0.22.0-beta arriba.
0.19.0-beta incompatible MotivoExclusion TarifaNoConfigurada El tarifario de un tributo que declara cerrada su cobertura no tiene ninguna tarifa, en ninguna vigencia, para el concepto: el concepto no es de su alcance y la línea se descarta en vez de rechazar el cálculo. Cualquier otra tarifa faltante —en un tributo de cobertura abierta, o un hueco de vigencia en un concepto que el tarifario sí incluye— sigue rechazando el cálculo con TarifaNoVigente. Ver 0.19.0-beta arriba.
0.19.0-beta incompatible MotivoExclusion ConvenioExime El convenio para evitar la doble imposición con el país de residencia fiscal del beneficiario fija tarifa cero para la retención. La línea se descarta, y la condición del convenio queda en la traza con este mismo motivo. Ver 0.19.0-beta arriba.
0.18.0-beta incompatible TipoTarifa PorMil Una tarifa cuyo valor se lee por mil. La Tarifa de la línea llega en ‰, no como fracción: 4.14 significa 4,14‰, y quien la trate como porcentaje (× 100) la muestra como 414 %. Viaja en TarifaUsada, LineaDesglose y LineaDescartada. Además del switch, rompe —más callado— a quien formatee la tarifa según su tipo: un tipo que no reconoce cae al caso por defecto y pierde la unidad sin error. Ver 0.18.0-beta arriba.
0.17.0-beta aditiva MotivoRechazoCalculo JurisdiccionNoEncontrada Una ubicación de la transacción (sede emisora, sede de contraparte o lugar de ejecución) no referencia ninguna jurisdicción vigente del país a la fecha, así que el cálculo no se puede computar. Antes ese caso no tenía desenlace: la traducción lanzaba, el mensaje moría en dead-letter y el solicitante esperaba indefinidamente — mismo defecto que 0.13.0-beta arregló para MotivoRechazoEntrega. El defecto está en los datos maestros de jurisdicciones del productor, no en la solicitud: no invita a reintentar sin corregirlos.
0.14.0-beta incompatible Naturaleza Provision La tercera dirección de un tributo frente al valor de la operación (qué es y qué se sigue de ella está en el propio enum, que es su sede). Viaja en LineaDesglose.Naturaleza y LineaDescartada.Naturaleza. Además del switch, rompe —más callado— a quien particione un desglose en dos grupos por naturaleza: la partición binaria deja las provisiones afuera sin error. Ver 0.14.0-beta arriba.
0.13.0-beta aditiva MotivoRechazoEntrega RegistroInexpresable El registro existe y la solicitud era válida, pero porta un valor sin equivalente conocido en un campo que el contrato necesita. Antes ese caso no tenía desenlace: la traducción lanzaba, el mensaje moría en dead-letter y el solicitante esperaba indefinidamente. A diferencia de RegistroNoEncontrado, no invita a reintentar: el defecto está en lo ya persistido del lado del productor.

Otras versiones agregaron eventos o campos, no miembros, y fueron aditivas sin reserva: 0.21.0-beta (LineaDesglose portó Familia y TributoDeColombia, los dos nullable con default; hoy Familia no viaja en el bus y DeColombia vive en Tributo, ver «DeColombia en Tributo» más abajo), 0.16.0-beta (evento saliente nuevo PerfilTributarioActualizado; ver «Aviso de cambio del perfil tributario» más abajo), 0.10.0-beta (CalculoTributarioRealizado portó ExplicacionDelCalculo, que 0.26.0-beta retiró) y 0.8.0-beta (los tres eventos del flujo de cálculo portan un Guid Correlacion, token opaco que el productor devuelve verbatim para que el solicitante descarte respuestas superseded; los mensajes legacy deserializan a Guid.Empty).

0.30.0-beta no agrega tipos ni miembros y es aditiva sin reserva: una solicitud con Ubicaciones o Conceptos ausentes deja de quedar sin respuesta. Antes la traducción lanzaba, el mensaje moría en dead-letter y el solicitante esperaba indefinidamente. Ahora CalculoTributarioRechazado sale con ComandoInvalido en un gravamen y con DatosFaltantes en la propuesta de un desgravamen sin conceptos; ConfirmacionTributariaRechazada sale con ComandoInvalido, salvo la confirmación de un desgravamen sin conceptos, que sale con DatosFaltantes. La propuesta de un desgravamen sin Ubicaciones sigue aceptándose. Los dos motivos ya los lee cualquier consumidor de 0.29.0-beta.

0.32.0-beta agrega un tipo y un campo opcional, y es aditiva sin reserva: Compartidos.Jurisdiccion(Codigo, Nombre) y JurisdiccionSubnacional al final de LineaDesglose, LineaDescartada, LineaDesgloseExplicada y LineaDescartadaExplicada. Es la jurisdicción en que el cálculo localizó el tributo de la línea, con el nombre que tenía al calcular, para mostrarla sin consultar el catálogo. Es null en los tributos nacionales, en los descartes previos a la localización (JurisdiccionNoAplica, ClasificacionExcluida), en los cálculos anteriores al campo y en la propuesta de un desgravamen: sus líneas se prorratean del desglose confirmado, que no congela la jurisdicción. La línea que confirma el consumidor (LineaConfirmada) no cambia, y la comparación contra el cálculo de referencia no usa el campo: confirmar sin cambios sigue sin contar como intervención.

Eventos

Namespace: Cosmos.Impuestos.Contratos.Eventos. Modelos compartidos en Cosmos.Impuestos.Contratos.Compartidos; enums en Cosmos.Impuestos.Contratos.Enums.

Evento Dirección Propósito
CalculoTributarioSolicitado entrante Solicita la cotización tributaria de unos conceptos. Impuestos resuelve el contexto y ejecuta el motor sin mutar estado; si trae OrigenDelDesgravamen, no ejecuta el motor y deriva el desglose por prorrateo de la transacción de origen.
CalculoTributarioRealizado saliente Resultado de éxito de la cotización (aplicados/descartados, sin prosa), correlacionado por ReferenciaOrigen.
CalculoTributarioRechazado saliente Resultado de rechazo de la cotización; contrapartida de CalculoTributarioRealizado, misma ReferenciaOrigen. Toda solicitud produce uno u otro.
SolicitarConfirmacionTributaria entrante Solicita confirmar un registro tributario (gravamen/desgravamen); para un desgravamen, Conceptos trae el monto devuelto de cada uno.
ConfirmacionTributariaRealizada saliente Resultado de éxito de la confirmación: el id del RegistroTributario recién persistido y la etiqueta de cada línea (LineaEtiquetada: su tributo y su concepto de origen). Correlacionado por ReferenciaOrigen.
ConfirmacionTributariaRechazada saliente Resultado de rechazo de la confirmación; contrapartida de ConfirmacionTributariaRealizada, misma ReferenciaOrigen. No persiste ningún registro.
SolicitarRegistroTributario entrante Consulta pura: pide el contenido de un registro tributario ya confirmado, por SubDominio + TransaccionId. Responde con RegistroTributarioEntregado o EntregaDeRegistroTributarioRechazada.
RegistroTributarioEntregado saliente El desglose confirmado del registro, a demanda: por cada línea, su tributo y sus montos (LineaEntregada). El resto del registro se consulta por HTTP. Correlacionado por ReferenciaSolicitud.
EntregaDeRegistroTributarioRechazada saliente Resultado de rechazo de la entrega; contrapartida de RegistroTributarioEntregado, misma ReferenciaSolicitud.
PerfilTributarioActualizado saliente Aviso de que el perfil tributario de un tercero cambió en algo que afecta el cálculo. No lleva el estado del perfil.

ReferenciaOrigen es una correlación opaca que define el consumidor (p. ej. su identificador de transacción) y que Impuestos eco-devuelve en el evento de respuesta para enrutar el resultado.

DeColombia en Tributo

Tributo es la identidad del tributo de una línea del bus, agrupada aparte de los montos. Lo que un proceso necesita para reconocerlo y tratarlo es Codigo, Nombre, Naturaleza, CaracterRetencion y DeColombia. La familia (FamiliaTributaria) y el mecanismo de recaudo (MecanismoDeRecaudo) no viajan por el bus desde 0.26.0-beta: se consultan en tributo.familia y tributo.mecanismoDeRecaudo de las dos consultas HTTP (TributoDeLectura). Familia sigue distinguiendo el impuesto de su retención junto con Naturaleza: RIVA e IVA comparten familia (ValorAgregado) pero tienen Naturaleza distinta (Sustractivo/Aditivo); para eso, en el bus, alcanza con Naturaleza y Codigo.

Tributo.DeColombia (TributoDeColombia): un miembro por código de tributo de la edición de Colombia (Iva, Riva, Rica, y así con cada tributo declarado en esa edición). Es append-only y viaja como entero en el bus, igual que cualquier otro enum de este contrato. Se deriva de Tributo.Codigo, que ya viaja congelado, así que también lo llevan los registros anteriores a la existencia de este campo — acá null no distingue "registro viejo" de "no es un tributo colombiano": significa que el código no coincide con ningún tributo de la edición de Colombia. El mapeo es por código y no por quién declaró el tributo ni en qué país: un tributo propio del tenant (de cualquier país, incluida Colombia) que reuse uno de esos códigos publica el mismo miembro que el tributo de la edición — ver «Riesgo aceptado» abajo.

Riesgo aceptado: el mapeo de DeColombia es por código y no conoce ni el país de la línea ni si el tributo es de la edición o propio del tenant. Un tenant de cualquier país —incluida Colombia— que da de alta un tributo propio con un código que coincide con uno de la edición de Colombia (p. ej. RIVA, o IVA para un tenant colombiano con su propio tributo de ese código) publica ese mismo miembro. Los consumidores de este campo operan hoy solo en Colombia, así que la colisión no tiene efecto observable adverso; se documenta acá para quien extienda esa integración a otro país o discrimine por este campo asumiendo que solo lo publica la edición.

Aviso de cambio del perfil tributario

PerfilTributarioActualizado se publica en el topic CanalImpuestos.TopicPerfilTributarioActualizado y lleva tres datos: la identificación del tercero (IdentificacionLegal), el id del perfil y FechaDelCambio. Es un aviso, no una transferencia de estado: no lleva calidades fiscales, actividades económicas ni razón social. Quien reaccione vuelve a pedir el cálculo, y el cálculo lee el perfil vigente.

  • Uno por comando, no uno por cambio: un comando que persiste varios cambios a la vez avisa una sola vez.
  • Solo cuando lo persistido afecta el cálculo. Un cambio de razón social no avisa, tampoco un aseguramiento del perfil que no cambió nada, y un comando rechazado no avisa nunca. La creación del perfil sí avisa: antes de ella el cálculo rechazaba por perfil no encontrado.
  • FechaDelCambio es cuándo quedó persistido el cambio, no la vigencia del dato tributario, que puede ser pasada o futura. Sirve para ordenar avisos, no para decidir desde cuándo rige el perfil.
  • El envelope lleva la identidad de quien originó el cambio: tenant, usuario y membresía de organización.
  • El aviso no reemplaza al recálculo a demanda, por dos razones. Hoy avisan los cambios que entran por la API HTTP, y los que entran por el servidor MCP de Impuestos todavía no. Y la entrega del aviso no está garantizada como atómica con el cambio: un cambio persistido puede quedar sin su aviso.

Consulta de la explicación de una cotización

Una cotización no se persiste como registro, pero deja su traza estructurada mientras la obligación sigue pendiente. La explicación se redacta al leer, con la misma redacción que el registro confirmado.

Una propuesta de desgravamen no deja traza de cotización, porque no hay motor que explicar: la consulta responde 404 igual que si la referencia no existiera. El desglose de un desgravamen se deriva por prorrateo del registro de origen, no del motor, así que no hay condiciones evaluadas ni descartes que narrar. Su procedencia se obtiene, una vez confirmado, por GET /registros-tributarios/{subDominio}/{transaccionId}.

GET /explicaciones-de-cotizacion/{referenciaOrigen}?correlacion={guid}

referenciaOrigen es la misma referencia opaca con que se solicitó el cálculo, sin reinterpretar; una referencia con / no matchea la ruta y da 404 del ruteo. correlacion es la aplicada por quien consume: la del cálculo cuyo desglose muestra, no la vigente.

Estado Cuándo
200 Hay traza para la referencia y su correlación coincide. El cuerpo es la explicación redactada, con la de las líneas descartadas.
400 Falta correlacion o no es un Guid.
404 No hay traza para la referencia, o la traza guardada es de otra correlación. Los dos casos son indistinguibles a propósito: la consulta falla cerrado y nunca devuelve prosa de un cálculo distinto al que se muestra.

Hay a lo sumo una traza por referencia: recotizar la reemplaza, así que un cliente con una correlación anterior recibe 404 y debe degradar. La traza se elimina, en la medida de lo posible, cuando se confirma el registro tributario de esa referencia (si la eliminación falla la confirmación sigue y la traza puede quedar); desde entonces la explicación se obtiene por GET /registros-tributarios/{subDominio}/{transaccionId}.

Desde 0.26.0-beta la traza es la única fuente de la explicación de una cotización: el evento ya no la lleva. Guardarla es un intento: si falla, CalculoTributarioRealizado se publica igual, porque los números valen más que la prosa, y esa cotización responde 404 en esta consulta. Recotizar la referencia la recupera. Quien necesite la explicación debe tratar el 404 como «sin explicación disponible» y no como un error.

Uso

using Cosmos.Impuestos.Contratos.Eventos;

// publicar una solicitud
await bus.PublishAsync(new CalculoTributarioSolicitado(referenciaOrigen, /* … */));

// consumir el resultado
public Task Handle(CalculoTributarioRealizado evento) { /* enrutar por evento.ReferenciaOrigen */ }

El nombre del servicio/cola productor para suscribirse a los eventos entrantes lo define el wiring del consumidor (SuscribirseAServicio).

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Cosmos.Impuestos.Contratos:

Package Downloads
Cosmos.Impuestos.Mensajeria

Extensiones de Wolverine para integrarse con Cosmos Impuestos sobre Azure Service Bus (topología topic-por-evento): publica las solicitudes tributarias a sus topics y, por separado, permite escuchar los resultados y broadcasts de Impuestos por una subscription propia. Publicar y escuchar son capacidades componibles para que un servicio de borde pueda solicitar sin suscribirse a los resultados.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.33.0-beta 46 10/2/2026
0.32.0-beta 99 10/1/2026
0.31.0-beta 84 10/1/2026
0.30.0-beta 42 9/30/2026
0.29.0-beta 41 9/30/2026
0.28.0-beta 43 9/30/2026
0.27.0-beta 118 9/30/2026
0.26.0-beta 87 9/29/2026
0.25.0-beta 133 9/28/2026
0.24.0-beta 61 9/28/2026
0.23.0-beta 103 9/27/2026
0.22.0-beta 67 9/27/2026
0.21.0-beta 65 9/26/2026
0.20.0-beta 57 9/25/2026
0.19.0-beta 145 9/24/2026
0.17.0-beta 138 9/21/2026
0.15.0-beta 96 9/16/2026
0.14.0-beta 267 8/21/2026
0.13.0-beta 388 8/5/2026
0.11.0-beta 77 7/28/2026
Loading failed

0.33.0-beta (incompatible) — MotivoRechazoCalculo gana ConfiguracionIncoherente al final, sin cambio de ordinales: la
configuración vigente existe pero se contradice o deja sin resolver qué debe pasar (dos condiciones que coinciden sin
llevar al mismo resultado, una sustitución cíclica, …); la corrige quien configura y reintentar no cambia el desenlace.
Varios rechazos que antes llegaban como Indeterminado llegan ahora con un motivo que dice quién corrige:
ConfiguracionIncoherente, ComandoInvalido (un dato que el tributo exige y la solicitud no trae, como el lugar de
ejecución de un tributo localizado o el concepto de pago de una tarifa por concepto de pago) y
ConfiguracionTributariaNoEncontrada (falta el valor de referencia de una unidad a la fecha de la transacción). Un
rechazo del cálculo llega a ConfirmacionTributariaRechazada con uno de dos motivos: ComandoInvalido cuando el defecto
es de la solicitud y DatosFaltantes para el resto, incluida la configuración incoherente; los demás motivos de la
confirmación no cambian.
Una solicitud con ids de concepto repetidos, que antes se calculaba, ahora se rechaza con ComandoInvalido en el cálculo
y en la confirmación de un gravamen. En la confirmación, la solicitud sin lugar de ejecución o sin concepto de pago
pasa de DatosFaltantes a ComandoInvalido; revierte el criterio de 0.29 para esos dos casos: el contrato permite omitir
el dato y solo quien arma la solicitud puede aportarlo. Rompe a quien discrimine Indeterminado, a quien discrimine
DatosFaltantes de ConfirmacionTributariaRechazada, a quien mande conceptos con el mismo id y a quien haga switch
exhaustivo sobre MotivoRechazoCalculo.

0.32.0-beta (aditiva) — cada línea del cálculo dice la jurisdicción subnacional de su tributo. Tipo nuevo
Compartidos.Jurisdiccion(Codigo, Nombre); LineaDesglose, LineaDescartada y las dos explicadas ganan
JurisdiccionSubnacional, opcional y al final. Es nula en los tributos nacionales, en los descartes anteriores a la
localización del tributo (JurisdiccionNoAplica, ClasificacionExcluida), en los cálculos anteriores al campo y en la
propuesta de un desgravamen, cuyas líneas se prorratean del desglose confirmado. La línea que confirma el consumidor no la trae ni la comparación contra el cálculo la usa: confirmar sin cambios no
cuenta como intervención.

0.31.0-beta (incompatible) — MotivoExclusion gana ActividadNoSujeta al final: la jurisdicción reconoce la actividad
del sujeto pasivo y la declara no sujeta, así que el tributo por actividad se descarta sin tarifa. Las cinco líneas
del cálculo (LineaDesglose, LineaConfirmada, LineaDescartada y las dos explicadas) ganan ActividadEconomica, opcional:
en una jurisdicción con catálogo de actividades FactorUtilizado pasa a ser la línea tarifaria y de ella no se
reconstruye la actividad. Rompe a quien haga switch exhaustivo sobre MotivoExclusion; quien confirma una línea que
trae actividad tiene que devolverla, porque la comparación contra el cálculo de referencia la incluye.

0.30.0-beta (aditiva) — una solicitud sin ubicaciones o sin conceptos recibe respuesta. Antes la traducción lanzaba y
el mensaje quedaba sin respuesta; ahora se rechaza con ComandoInvalido en el cálculo de un gravamen y en la
confirmación, salvo los Conceptos ausentes de un desgravamen, que van a DatosFaltantes igual que la propuesta de un
desgravamen sin Conceptos. La propuesta de un desgravamen sin Ubicaciones sigue aceptándose. No cambia ningún
tipo ni miembro.

0.29.0-beta (incompatible) — MotivoRechazoCalculo gana ComandoInvalido al final: la solicitud que no cumple el contrato.
Lo producen siete causas: el identificador del concepto vacío, su clasificación tributaria ausente o vacía, su
actividad económica informada vacía, la identificación emisora ausente, la de la contraparte ausente, la fecha de
la transacción por defecto y la lista de conceptos vacía en un gravamen. DatoInvalido queda sin productor y se conserva porque su
ordinal está publicado. La confirmación pasa esos casos de DatosFaltantes a ComandoInvalido; las demás fallas del
motor siguen en DatosFaltantes. Las métricas de Indeterminado bajan. Rompe a quien discrimine DatoInvalido para la
actividad informada vacía o DatosFaltantes en la confirmación de una solicitud mal armada, y a quien haga switch
exhaustivo sobre MotivoRechazoCalculo. Guía en el README.

0.28.0-beta (incompatible) — MotivoRechazoCalculo gana DatosFaltantes; la confirmación de un desgravamen rechaza
fuera del margen. La propuesta de un desgravamen sin conceptos devueltos rechaza con DatosFaltantes en vez de
DatoInvalido (que se conserva para otras formas inválidas). ConfirmacionTributariaRechazada empieza a llevar
IntervencionExcedeMargen cuando el desglose confirmado de un desgravamen se aparta de su prorrateo en más de una
unidad del último decimal de la escala del país, o en cualquier diferencia de tarifa, tipo, factor o línea. En un
gravamen, una diferencia dentro de ese margen deja de marcar HuboIntervencion. Rompe a quien discrimine
DatoInvalido para el desgravamen sin conceptos y a quien haga switch exhaustivo sobre MotivoRechazoCalculo.
Guía en el README.

0.27.0-beta (incompatible) — el concepto informa la actividad económica y el motor no desempata entre varias.
ConceptoCalculo gana ActividadEconomica (opcional): la actividad del sujeto pasivo que corresponde al concepto.
LineaDescartada gana ActividadRechazada y ActividadesCandidatas (opcionales). MotivoExclusion gana
ActividadAmbigua al final: con varias actividades candidatas y ninguna informada, el tributo por actividad se
descarta con las candidatas en orden ordinal; quien consume indica una en el concepto y recalcula. Rompe a quien
haga switch exhaustivo sobre MotivoExclusion. Guía en el README.

0.26.0-beta (incompatible) — los eventos de salida publican solo lo que un proceso usa al recibirlos.
CalculoTributarioRealizado pierde la prosa (ExplicacionDelCalculo, y RazonDeAplicacion y Explicacion de cada
línea). ConfirmacionTributariaRealizada queda en ReferenciaOrigen, RegistroTributarioId y DesgloseConfirmado
como lista de LineaEtiquetada (tributo y concepto de origen); RegistroTributarioEntregado, en ReferenciaSolicitud,
RegistroTributarioId y DesgloseConfirmado como lista de LineaEntregada (tributo y montos). Tributo pierde Pais,
Familia y MecanismoDeRecaudo, y Codigo pasa a ser su primer parámetro. Se borran los tipos que quedaron sin uso
(EntidadFiscal, AtributoSnapshot, Jurisdiccion, ConfiguracionUsada, TarifaUsada, CondicionEvaluada,
IndiceDeReferenciaUsado, ConvenioUsado, CuantiaMinima y los enums UnidadDeReferencia y ResultadoEvaluacion).
Rompe fuente. En el cable, lo que se conserva mantiene su ruta, y lo retirado llega sin aviso: los valores en
cero o false (un dato falso que parece válido) y los objetos nulos. Lo que salió se consulta por HTTP, y las dos consultas no cambian de forma. Guía completa en el README.

0.25.0-beta (incompatible) — LineaDesglose y LineaDescartada reemplazan sus campos sueltos de identidad
(CodigoTributo, NombreTributo, Naturaleza, CaracterRetencion, MecanismoDeRecaudo, Familia,
TributoDeColombia) por un único objeto Tributo { Pais, Codigo, Nombre, Familia, Naturaleza,
CaracterRetencion, MecanismoDeRecaudo, DeColombia }. Es el primer parámetro posicional de los dos records:
rompe a quien construya una LineaDesglose/LineaDescartada y cambia la forma serializada para quien lea por
clave. Fuera de Colombia la identidad del tributo es (Pais, Codigo), sin campo por país. Absorbe #362:
LineaDescartada ahora lleva Familia y DeColombia, que antes solo tenía la aplicada. La entrada también
cambia: SolicitarConfirmacionTributaria.DesgloseConfirmado pasa a ser una lista de LineaConfirmada, una
línea angosta que nombra el tributo solo por CodigoTributo; Impuestos completa el Tributo desde el cálculo de
referencia, sus descartes o su catálogo (intervención manual), y rechaza con ComandoInvalido un código que no
conoce. Corte directo, sin período de convivencia. Guía completa y riesgo aceptado del mapeo de DeColombia por código en el README.

0.24.0-beta (incompatible) — quien ya confirma desgravámenes con SolicitarConfirmacionTributaria tiene que
shippear código, sin tocar ningún campo nuevo: Conceptos pasa a leerse y no puede llegar vacío (antes no
se leía; vacío rechaza con DatosFaltantes, y también rechaza con DatosFaltantes un concepto que viene en
DesgloseConfirmado sin su monto en Conceptos), DesgravamenExcedeSaldo gana productor (antes
se confirmaba igual), y la referencia se deriva de Conceptos con cierre por residuo en vez del
DesgloseConfirmado sin más. El camino de cálculo sí es aditivo: CalculoTributarioSolicitado gana
OrigenDelDesgravamen ({ SubDominio, TransaccionId }), opcional y al final; si viene, la solicitud es la
cotización de un desgravamen: Impuestos no ejecuta el motor, busca el RegistroTributario de la transacción
de origen y deriva el desglose por prorrateo (un factor por concepto, R((valor × devuelto) ÷ gravado),
hijos incluidos, con el redondeo del país). MotivoRechazoCalculo gana OrigenNoEncontrado,
ConceptoNoExisteEnOrigen y DesgravamenExcedeSaldo (al final, ordinales 8 a 10). Cada desgravamen confirmado
descuenta de un saldo por transacción de origen (compra o venta desgravada): proponer o confirmar un monto
que, sumado a lo ya devuelto de un concepto, supere lo que ese concepto se gravó se rechaza en vez de
aceptarse (por monto del concepto, no por tributo dentro del concepto); la devolución que completa un concepto revierte, en la
referencia, el valor exacto que le queda a cada tributo (residuo). Guía en el README.

0.23.0-beta (incompatible) — MotivoRechazoCalculo gana ClasificacionNoVigente (al final, ordinal 7). Un
concepto cuya clasificación no existe o no está vigente a la fecha de la transacción rechaza el cálculo en
vez de devolver un desglose en cero con todo descartado por ClasificacionExcluida. Además se retira la
homologación de 0.20.0-beta: clasificaciones, condiciones, regímenes y definiciones de calidad viajan con el
código que publica erp-definiciones, tal cual (GRAV_19, perteneceRegimenIVA, RTF-01a, ZF-BAQ), sin tabla de
traducción. Guía en el README.

0.22.0-beta (incompatible) — FechaTransaccion pasa de DateTimeOffset a DateOnly en
CalculoTributarioSolicitado y en ContextoTransaccional (SolicitarConfirmacionTributaria,
RegistroTributarioEntregado, ConfirmacionTributariaRealizada). En el cable es "yyyy-MM-dd"; un mensaje
con hora ya no deserializa y no recibe respuesta (ni CalculoTributarioRechazado ni
ConfirmacionTributariaRechazada). El modelo de dominio de Impuestos en erp-definiciones tipa la fecha
como date: con un instante, el día se leía con el reloj del offset y el mismo instante desde otra zona
podía caer en otro día (el 31 de diciembre, en otro año de UVT). PerfilTributarioActualizado.FechaDelCambio
sigue siendo un instante. Guía en el README.

0.21.0-beta (aditiva) — LineaDesglose gana dos campos nullable al final: Familia (FamiliaTributaria:
ValorAgregado, Consumo, Renta, ActividadEconomica, Contribucion, Otro), la familia del tributo, igual en
los tres países; y TributoDeColombia (TributoDeColombia), el miembro cuyo código coincide con el de un
tributo de la edición de Colombia, null en cualquier otro caso (el mapeo es por código, no por si el
tributo es de la edición o propio del tenant: ver README para el riesgo aceptado de colisión). Los dos
enums son append-only y viajan como entero en el bus. Un registro anterior al congelamiento de Familia la
publica null; Familia sí se congela en la línea, TributoDeColombia se deriva del código ya congelado y por
eso también lo llevan los registros anteriores. Guía completa en el README.

0.20.0-beta (incompatible) — la configuración estándar sale de ediciones publicadas. Los códigos de
clasificaciones, condiciones, regímenes, definiciones y calidades fiscales que viajan en los eventos son
los internos de la tabla de homologación, no el código del libro (revertido en 0.23.0-beta); toda vigencia
es [desde, hasta) con hasta = el primer día en que deja de regir. Se retira la réplica de clasificaciones por el
bus (ClasificacionTributariaPublicada, ClasificacionTributariaDesactivada, ReplicaDeClasificacionesSolicitada y sus
topics), que ningún servicio consumía. Guía de migración en el README.

0.19.0-beta (incompatible) — MotivoExclusion gana TarifaNoConfigurada y ConvenioExime (al final,
ordinales 8 y 9). Viajan en LineaDescartada y CondicionEvaluada. Rompe a quien rotule el descarte según
su motivo: el que no reconoce cae al rótulo por defecto sin error. Aditivo en la misma versión:
TarifaUsada porta ConvenioUsado? Convenio (país contraparte, nombre e instrumento normativo del
convenio que fijó la tarifa), nullable con default.

0.18.0-beta (incompatible) — TipoTarifa gana PorMil (al final, ordinal 2). Su Tarifa llega en ‰
(4.14 = 4,14‰), no como fracción: multiplicarla por cien la muestra como 414 %. Viaja en TarifaUsada,
LineaDesglose y LineaDescartada. Rompe a quien haga switch exhaustivo sin arm por defecto y, sin
error, a quien formatee la tarifa según su tipo: el tipo que no reconoce cae al caso por defecto y
pierde la unidad.

0.17.0-beta (aditiva) — MotivoRechazoCalculo gana JurisdiccionNoEncontrada (al final, ordinal 6).
Un cálculo cuya ubicación no resuelve jurisdicción ahora publica CalculoTributarioRechazado en vez
de morir en dead-letter.

0.16.0-beta (aditiva) — PerfilTributarioActualizado, un evento saliente nuevo con su topic
(CanalImpuestos.TopicPerfilTributarioActualizado).

Avisa que el perfil tributario de un tercero cambió en algo que afecta el cálculo. Lleva solo la
identificación del tercero, el id del perfil y la fecha del cambio: NO lleva el estado del perfil, así
que quien reaccione vuelve a pedir el cálculo. Se emite uno por comando, no uno por cambio. Un cambio de
razón social no avisa, un aseguramiento que no movió nada tampoco, y la creación del perfil sí.
FechaDelCambio es cuándo quedó persistido el cambio, no la vigencia del dato tributario. El aviso no
reemplaza al recálculo a demanda, por dos razones: hoy avisan los cambios que entran por la API HTTP y no
los que entran por el servidor MCP de Impuestos, y la entrega del aviso no está garantizada como atómica
con el cambio.

No cambia ningún tipo existente. Quien venga de 0.14.0-beta o anterior pasa por 0.15.0-beta, que SÍ es
incompatible: ver README.md.

Historial completo de cambios incompatibles, y el índice de los miembros de enum agregados: README.md.