From 4ccc09bed030e4ed1eee104596691319ffa3df39 Mon Sep 17 00:00:00 2001 From: LauAubert Date: Fri, 2 Oct 2026 01:52:13 -0300 Subject: [PATCH] fix: [TESIS-999017] refuse fractional order quantities with 422 instead of 500 OrderItem validated the quantity as greater than zero on the raw value, and the integer column then cast it: 0.5 passed, was stored as 0 and made DeductStock raise ArgumentError, which nobody rescues, so POST /orders answered 500. 2.7 was silently truncated to 2. The edition already refused both through ReplaceOrderLines#positive_integer; the creation did not. The quantity of an order item is now validated as an integer, so the creation and the webhook ingestion answer 422 with the reason before any stock moves. Integers that come as text from a form are still accepted. Co-Authored-By: Claude Opus 5.5 --- app/models/order_item.rb | 6 +++++- spec/models/order_item_spec.rb | 13 +++++++++++++ spec/requests/api/v1/orders_spec.rb | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/models/order_item.rb b/app/models/order_item.rb index ee32c6cf..c7346d8a 100644 --- a/app/models/order_item.rb +++ b/app/models/order_item.rb @@ -8,7 +8,11 @@ class OrderItem < ApplicationRecord # una modificación que tenga que devolverle unidades no sabe a dónde. belongs_to :warehouse, optional: true - validates :quantity, numericality: { greater_than: 0 } + # Entero: la columna es integer y guardaba `0.5` como 0 y `2.7` como 2. El + # primero pasaba la validación (0,5 > 0) y reventaba después en DeductStock + # con un 500; el segundo se truncaba sin aviso. Se valida antes de escribir, + # así el alta y el webhook responden 422 con el motivo. + validates :quantity, numericality: { only_integer: true, greater_than: 0 } validates :unit_price, numericality: { greater_than_or_equal_to: 0 } validate :product_belongs_to_same_company_as_order validate :warehouse_belongs_to_same_company_as_order diff --git a/spec/models/order_item_spec.rb b/spec/models/order_item_spec.rb index d38b3ab0..aa02c12d 100644 --- a/spec/models/order_item_spec.rb +++ b/spec/models/order_item_spec.rb @@ -40,6 +40,19 @@ expect(order_item).not_to be_valid end + # La columna es integer: 0,5 se guardaba como 0 y 2,7 como 2. + it 'refuses a fractional quantity', :aggregate_failures do + [0.5, 2.7, '1.5'].each do |quantity| + order_item.quantity = quantity + expect(order_item).not_to be_valid, "#{quantity.inspect} should be refused" + end + end + + it 'accepts an integer that comes as text from a form' do + order_item.quantity = '3' + expect(order_item).to be_valid + end + it 'validates unit_price is not negative' do order_item.unit_price = -1 expect(order_item).not_to be_valid diff --git a/spec/requests/api/v1/orders_spec.rb b/spec/requests/api/v1/orders_spec.rb index 63f3ddb5..3cafee14 100644 --- a/spec/requests/api/v1/orders_spec.rb +++ b/spec/requests/api/v1/orders_spec.rb @@ -396,6 +396,21 @@ def order_of_another_company expect(response).to have_http_status(:unprocessable_content) end + # Pasaba la validación (0,5 > 0), la columna integer la guardaba en 0 y + # DeductStock reventaba con ArgumentError: 500. + it 'rejects a quantity below one unit with 422, not 500', :aggregate_failures do + post_order(build_payload(items: [default_item.merge(quantity: 0.5)])) + + expect(response).to have_http_status(:unprocessable_content) + expect(response.parsed_body['error']).to include('must be an integer') + end + + it 'rejects a fractional quantity instead of truncating it', :aggregate_failures do + expect { post_order(build_payload(items: [default_item.merge(quantity: 2.7)])) } + .not_to change(Order, :count) + expect(response).to have_http_status(:unprocessable_content) + end + it 'rejects when warehouse belongs to another company' do other_wh = other_company_warehouse post_order(build_payload(items: [default_item.merge(warehouse_id: other_wh.id)]))