ADR-015: order-service valida el precio con su propia vista de precios
Aceptado el 10/11/2026 por Andrés. Complementado por ADR-020 (vigencias del precio; la ventana de tolerancia y la corrección no cambian).
Contexto
docs/to-be/04-saga-crear-orden.md dice que la capa es la autoridad del precio: una orden se acepta si su unitPrice coincide con el precio vigente o con uno que estuvo vigente dentro de una ventana de tolerancia; si no, 422 PRICE_MISMATCH. El diseño no decía de dónde saca order-service ese precio, y las reglas de arquitectura cierran las salidas obvias:
- No puede llamar a
pricing-service: la única llamada síncrona interna permitida esorder-serviceainventory-servicepor gRPC (regla 2 deCLAUDE.md, ADR-007). - No puede leer el Redis de
pricing-service: una base por servicio (regla 6). - No puede llamar al ERP: solo los adaptadores hablan con el legado (regla 1).
Además la ventana de 15 minutos no tenía sustento. No absorbe el retraso del dato (que el supuesto 6 fija en un minuto como máximo), sino el tiempo entre que el canal muestra el precio y envía la orden: es una política comercial de protección de precio.
Opciones
| Opción | A favor | En contra |
|---|---|---|
A. order-service consume andes.pricing.prices.v1 y guarda su vista con historial |
Respeta las reglas; la validación no depende de otro servicio en línea | Una tabla más en order-service; la vista es eventualmente consistente |
B. gRPC de order-service a pricing-service |
Una sola fuente en línea | Rompe la regla 2 y suma una dependencia síncrona a crear orden |
C. Leer el Redis de pricing-service |
Sin copia | Rompe la regla 6; Redis no guarda historial |
| D. Confiar en el precio del canal | Sin trabajo | La capa deja de ser la autoridad del precio; un partner puede vender a cualquier precio |
E. Incluir el precio en la respuesta de Reserve |
Reusa la llamada que ya existe | Mete precios en inventory-service, que no es su dominio |
Decisión
Opción A.
order-serviceconsumeandes.pricing.prices.v1con su propio grupo (order-service) y guarda la tablaprice_viewen su base: llavesku|channel|priceList,priceVersion,salePrice,currency, momento en que la versión llegó y momento en que fue reemplazada. Deduplica porce_id, como todo consumidor.- La ventana de tolerancia es igual al vencimiento de la reserva del canal (
docs/to-be/03-stock-vendible-y-reservas.md): 15 minutos por defecto, configurable por canal en la misma política. Un solo concepto, la ventana de pago: mientras la reserva sigue viva, el stock y el precio quedan sostenidos el mismo tiempo. Un canal con un pago más largo sube los dos juntos. - Se acepta la orden si
unitPricecoincide con la versión vigente, o con una versión reemplazada hace menos que la ventana del canal. Si la orden traepriceVersion, se compara esa versión. - Corrección de precio: el evento
PriceChangedgana el campo opcionalcorrection(booleano,falsepor defecto). Si llegatrue, las versiones anteriores de esa llave dejan de aceptarse de inmediato, aunque estén dentro de la ventana. Cubre el precio mal cargado a la baja, que de otro modo se seguiría vendiendo durante toda la ventana. El erp-adapter lo marca según el motivo del cambio en el ERP (supuesto 15). - SKU sin precio en la vista:
422 UNKNOWN_SKU. - Las versiones reemplazadas hace más que la ventana más larga de los canales se borran.
- Reconstrucción: el tópico es compactado, así que al releerlo desde cero solo queda la última versión de cada llave. Para que el historial dentro de la ventana sobreviva, el canal declara
min.compaction.lag.msde una hora en sus bindings (ADR-017). La ventana de un canal no puede pasar de una hora.
Es un cambio compatible del contrato: un campo opcional nuevo y un consumidor más.
Verificación
Se construye en F4, con order-service:
- Prueba de la vista: precio vigente aceptado; versión reemplazada dentro de la ventana aceptada; fuera de la ventana,
422 PRICE_MISMATCHcon el precio vigente; versión anterior a una corrección, rechazada aunque esté dentro de la ventana. - La ventana cambia con la política del canal sin desplegar de nuevo.
make lint-asyncapicon el campocorrectiony el consumidor nuevo.
Qué se sacrifica
- Dos copias del precio (Redis de
pricing-serviceyprice_view), ambas derivadas del mismo tópico. Pueden diferir unos segundos; la que decide es la deorder-service. - Durante la ventana se vende a un precio que ya no rige, salvo que el cambio venga marcado como corrección. Es la política elegida: protege la conversión del canal a cambio de ese margen.
- La corrección depende de que el ERP registre el motivo del cambio. Si no lo hace, el erp-adapter no la puede marcar y el riesgo 9 queda abierto.