Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions app/poros/orders/process_webhook_order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class UnmappedProductError < StandardError; end
MISSING_ITEMS = 'the payload does not carry any order item'
UNREADABLE_ITEMS = 'the template could not read %<count>d of the order items in the payload'
ORDERS_UNIQUE_INDEX = 'index_orders_on_company_id_and_external_order_id'
CANCELLED = 'cancelled'

def initialize(webhook_log:)
super()
Expand Down Expand Up @@ -82,11 +83,26 @@ def create_order(items)
# transacción, así que el orden no abre ninguna ventana.
def register_item(order, item, mapping)
quantity = quantity_of(item)
stock = Catalog::DeductStock.new(product: mapping.product, quantity: quantity).call
OrderItem.create!(order: order, product: mapping.product, warehouse_id: stock.warehouse_id,
OrderItem.create!(order: order, product: mapping.product,
warehouse_id: take_units(order, mapping.product, quantity),
quantity: quantity, unit_price: unit_price_of(item, mapping))
end

# Una venta que llega ya cancelada (en Mercado Libre la primera notificación
# puede traer un pago rechazado) se registra —queda el rastro de que existió—
# pero no se lleva stock: no va a salir. Antes se descontaba igual, y como una
# orden cancelada no se edita ni se vuelve a cancelar, esas unidades no
# volvían nunca y los canales publicaban de menos.
#
# Sin descuento no hay depósito que registrar: la línea queda sin él, igual
# que las anteriores a TESIS-126, y como la orden ya está cancelada nada va a
# intentar devolverle unidades.
def take_units(order, product, quantity)
return if order.status == CANCELLED

Catalog::DeductStock.new(product: product, quantity: quantity).call.warehouse_id
end

# Resuelve el producto interno de cada ítem antes de escribir nada: un ítem
# sin mapear corta el procesamiento acá, no a mitad de la orden.
#
Expand Down
1 change: 1 addition & 0 deletions docs/adr/ADR-010-ingesta-de-ordenes-de-webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Una venta que entra mal es peor que una venta que no entra: queda escrita, con m
| `unit_price` en el payload **y** `external_price` en el mapping (los dos) | Corta | Un ítem en 0 es indistinguible de una bonificación legítima: el error queda enterrado en un registro financiero y ya no se puede detectar |
| Un id externo sin `ProductMapping` | Corta (`UnmappedProductError`) | Es el caso que la DLQ resuelve sola: se crea el mapeo y se reintenta |
| `status` ausente o desconocido para el OMS | **No corta**: entra como `pending` | El estado es informativo y se corrige después; la venta es el dato que no se puede perder |
| `status` que llega `cancelled` | **No corta**: se registra cancelada, **sin descontar stock** y con las líneas sin depósito | La venta no va a salir. Descontar dejaba las unidades fuera para siempre, porque una orden cancelada no se edita ni se vuelve a cancelar (TESIS-999016) |
| Datos del comprador (documento, dirección, CP) | **No corta** | La plantilla los mapea si el canal los manda; su ausencia no mueve ni stock ni dinero |

El precio tiene un respaldo antes de cortar: el `external_price` del `ProductMapping`, o sea el precio publicado en ese canal. Es un precio real de la venta y no uno inventado, y cubre el caso común de un canal que no manda el precio en el webhook. Un `0` que **sí** viene en el payload se respeta: ahí el canal está afirmando que el ítem fue bonificado.
Expand Down
55 changes: 55 additions & 0 deletions spec/poros/orders/process_webhook_order_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,61 @@ def unique_violation
end
end

# La primera notificación de una venta puede traerla ya cancelada (en ML, un
# pago rechazado). Antes descontaba igual, y como una orden cancelada no se
# edita ni se vuelve a cancelar, esas unidades no volvían nunca.
context 'when the sale arrives already cancelled' do
subject(:process) { described_class.new(webhook_log: cancelled_log) }

def cancelled_log
@cancelled_log ||= create_log(order_payload(items: [line('MLA-1', 2, 100)],
status: 'cancelado'))
end

def create_service(mapper: order_mapper)
Service.create!(service_name: "Mercado Libre #{SecureRandom.hex(4)}", type: 'ecommerce',
http_method: 'GET', uri: 'https://api.ml.test/orders',
response_mapper: mapper,
response_value_mapper: { 'pagado' => 'paid', 'cancelado' => 'cancelled' })
end

before { publish('SKU-1', 'MLA-1', stock: 20) }

it 'records the sale as cancelled, so it leaves a trace', :aggregate_failures do
order = process.call

expect(order.status).to eq('cancelled')
expect(order.order_items.sum(:quantity)).to eq(2)
end

it 'takes no units from the stock' do
process.call

expect(stock_of('SKU-1')).to eq(20)
end

it 'marks the log as processed' do
process.call

expect(cancelled_log.reload.status).to eq('processed')
end

# Ya cancelada, nada va a intentar devolverle unidades.
it 'leaves its lines without a warehouse, since nothing was taken from any' do
expect(process.call.order_items.pluck(:warehouse_id)).to eq([nil])
end
end

context 'when the sale arrives paid' do
before { publish('SKU-1', 'MLA-1', stock: 20) }

it 'still takes its units from the stock' do
described_class.new(webhook_log: create_log(order_payload(items: [line('MLA-1', 2, 100)]))).call

expect(stock_of('SKU-1')).to eq(18)
end
end

context 'when the channel reports a status the OMS does not know' do
before { publish('SKU-1', 'MLA-1') }

Expand Down
Loading