feat: [TESIS-102] add product category and the primary warehouse to the list - #58
Open
TomasMartin2004 wants to merge 1 commit into
Open
feat: [TESIS-102] add product category and the primary warehouse to the list#58TomasMartin2004 wants to merge 1 commit into
TomasMartin2004 wants to merge 1 commit into
Conversation
…he list Closes the two data gaps that block the Master Catalog table in TESIS-62, and that also reach TESIS-63, where the category Select is rendered disabled because there is nothing behind it. Category is a nullable string with a vocabulary in Product::CATEGORIES — Electronics, Machinery, Cabling and Power, the values the designs show — validated by inclusion with allow_nil, since the products that already exist have no category and there is nothing to infer one from. Deliberately no CHECK constraint, which is a departure from what the rest of the schema does and worth stating. Every column that carries one today (orders.status, services.type, failed_events.status, shipments.status) holds a system vocabulary: a state machine the application transitions through, where the engine has to be the last guarantee because batch writes bypass the model. A product category is a business taxonomy the user picks and the list will grow. A CHECK there would mean a migration per new category, which buys nothing the model validator does not already give. The list gains primary_warehouse and warehouse_count, not the full breakdown. The Location Node column shows one node, and GET /products/:id already returns every warehouse — keeping the list serializer lighter than the detail one is the reason both exist. Primary is the warehouse holding the most units, ties broken by the lowest warehouse id. Without that tie-break two warehouses holding the same amount would return whichever Postgres finds convenient, and the column would change between two refreshes with nothing having happened. Rows holding zero units are skipped, so warehouse_count agrees with primary_warehouse: when the count is 0 the node is null, which is not the same as having 0 units in a known warehouse. The index preloads. preload and not includes: includes lets Rails choose between preloading and joining, and here the join breaks — with_total_stock already groups by products.id with its own SELECT, so resolving the association as eager_load would add the stocks and warehouses columns to that SELECT and Postgres would reject the query for columns outside the GROUP BY. preload guarantees the separate queries. GET /api/v1/products/categories exposes the vocabulary so the create modal and the list filter do not restate it in the frontend. Seeds assign categories outside the find_or_create_by! block, which only runs on create, so databases seeded before the column existed also get them. Out of scope, and named rather than dropped: the "In Transit" tab and the "+500 Incoming" hint of TESIS-62 are not missing fields but a domain that does not exist — there is no model for transfers between nodes, nor for purchase orders or receipts. Not verified locally: this machine cannot reach its database. The password in config/database.yml is not the one the installed PostgreSQL 18 accepts, so psql fails authentication for the postgres user and both db:migrate and rspec die on connection. db/schema.rb was updated by hand to match the migration. Verification is CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 Ticket de Jira
TESIS-102 — bloquea a TESIS-62
📝 Descripción
Cierra los dos agujeros de datos que hoy impiden implementar la tabla del Master Catalog (TESIS-62), y que además alcanzan a TESIS-63, donde el Select de categoría se pinta deshabilitado porque no hay nada detrás.
Salió de auditar TESIS-62: de sus 8 columnas,
Categoryno tenía columna enproducts, yLocation Nodeno tenía forma de resolverse porqueProductListSerializerno expone ninguna referencia a depósitos.Categoría
products.category, string nullable, con vocabulario enProduct::CATEGORIES—Electronics,Machinery,Cabling,Power, los valores que muestran los diseños — validado por inclusión conallow_nil, porque los productos que ya existen no tienen categoría y no hay con qué inferirla.Deliberadamente sin CHECK constraint, y vale explicarlo porque se aparta de lo que hace el resto del esquema. Todas las columnas que hoy tienen uno (
orders.status,services.type,failed_events.status,shipments.status) llevan un vocabulario de sistema: máquinas de estado que la aplicación transiciona, donde el motor tiene que ser la última garantía porque las escrituras por lote saltean el modelo. Una categoría de producto es taxonomía de negocio: la elige el usuario y la lista va a crecer. Un CHECK ahí obligaría a una migración por cada categoría nueva, y no compra nada que el validador de modelo no dé ya.Nodo principal en el listado
El listado suma
primary_warehouseywarehouse_count, no el desglose completo. La columna Location Node muestra un nodo, yGET /products/:idya devuelve todos los depósitos: mantener el serializer de listado más liviano que el de detalle es la razón de que existan los dos.Primario es el depósito con más unidades, desempatando por el
warehouse_idmás bajo. Sin ese desempate, dos depósitos con la misma cantidad devolverían el que a Postgres le convenga y la columna cambiaría entre dos refrescos sin que haya pasado nada — hay un spec dedicado a eso. Las filas en 0 se saltean, así quewarehouse_countes coherente conprimary_warehouse: cuando el conteo es 0 el nodo esnull, que no es lo mismo que tener 0 unidades en un depósito conocido.preloady noincludesEl
indexprecarga, y la elección es load-bearing.includesdeja que Rails elija entre precargar y hacer JOIN; acá el JOIN rompe:with_total_stockya agrupa porproducts.idcon su propioSELECT, así que resolver la asociación comoeager_loadsumaría las columnas destocksywarehousesa ese SELECT y Postgres rechazaría la consulta por columnas fuera delGROUP BY.preloadgarantiza las consultas separadas.Vocabulario para el Select
GET /api/v1/products/categoriesexponeProduct::CATEGORIESpara que el modal de alta y el filtro del listado no repitan la lista en el frontend.🛠️ Cambios realizados
add_category_to_products: columnacategorye índice(company_id, category)— compuesto y no sobrecategorysola, porque eldefault_scopedeCompanyScopedya ponecompany_iden el WHERE de toda consulta del listadoProduct: constanteCATEGORIES, validación de inclusión conallow_nil, y#primary_stock— que ordena en Ruby a propósito, porque quien llama ya precargóstocks: :warehousey unorderahí dispararía una query por filaProductSerializeryProductListSerializer:categoryProductListSerializer:primary_warehouse({ id, name, quantity }onull) ywarehouse_countProductsController:preload(stocks: :warehouse)enindex,categoryenproduct_params, y la accióncategoriesconfig/routes.rb:get :categories, on: :collectiondentro deproductsfind_or_create_by!—que sólo corre al crear— para que las bases ya sembradas antes de que existiera la columna también las reciban, con unifque mantiene la idempotenciaprimary_stock(sin stock, más unidades, desempate, filas en 0, todas en 0), 3 de categoría, y en request el nodo principal, el conteo, el desempate, los ceros, el nodo nulo, el N+1 de warehouses y el endpoint de categorías🧪 Cómo probar
bin/rails db:migrate→ agrega la columna y el índicebin/rails db:seeddos veces → idempotente; los productos de Norte quedan enElectronicsy los de Sur enMachineryGET /api/v1/products→ cada fila traecategory,primary_warehouseywarehouse_countGET /api/v1/products/categories→{ "data": ["Electronics", "Machinery", "Cabling", "Power"] }POST /api/v1/productsconcategory: "Groceries"→ 422; concategory: "Electronics"→ 201primary_warehousees siempre el mismo📸 Evidencia visual
N/A (backend).
Validación reproducida local:
bundle exec rspec→ 471 examples, 0 failuresbundle exec rubocop --force-exclusion→ 122 archivos, sin ofensasbin/brakeman -q→ No warnings founddb/schema.rbidéntico al del commitprimary_warehousees un objeto de tres claves, no el array de depósitos: la diferencia conProductSerializerse mantiene a propósito.indexpasa de 2 a 4 consultas por página (productos + total + stocks + warehouses), constantes, no por fila. Hay un spec que lo fija.