39 lines
2.8 KiB
Markdown
39 lines
2.8 KiB
Markdown
# F-102 — Peso unitario, compra mínima y límites de envío por peso (redefinida por el operador)
|
||
|
||
Redefinición de intake (work/current.md, 2026-08-21): el "pack" es en realidad un
|
||
**selector de compra mínima** por producto (el frontend bloquea la compra por debajo).
|
||
El peso del pedido es cantidad × peso unitario. Además, nuevos límites por método de
|
||
envío: **max weight** y **límite de envío gratuito por rango de peso**.
|
||
|
||
## Backend
|
||
- Migración 038: `catalog_products.unit_weight_kg numeric(8,3) NOT NULL DEFAULT 1`,
|
||
`catalog_products.min_purchase_qty integer NOT NULL DEFAULT 1`,
|
||
`shipping_methods.max_weight_kg` y `shipping_methods.free_shipping_max_weight_kg` (nullable).
|
||
- Dominio `Product`: campos `unitWeightKg` y `minPurchaseQty` con defaults en repositorio (`Number(row.unit_weight_kg ?? 1)`).
|
||
- `catalog.routes.ts`: validación zod en create/patch (`unitWeightKg` 0..1000, `minPurchaseQty` entero 1..999) y expuestos en `serializeProduct`.
|
||
- Dominio `ShippingMethod`: `maxWeightKg` y `freeShippingMaxWeightKg` (null = sin límite).
|
||
- `ShippingService.calculate(cartTotalCents, address, cartWeightKg = 0)`:
|
||
- excluye métodos cuyo `maxWeightKg` < peso del carrito;
|
||
- el envío gratis por umbral solo aplica si el peso está dentro del rango gratuito del método.
|
||
- Rutas shipping: create/patch/list exponen ambos límites; `POST /shipping/calculate` acepta `cartWeightKg` opcional.
|
||
- Checkout: nuevo dep `getCartWeightKg` calcula peso del carrito (cantidad × unit_weight_kg con fallback 1) y lo pasa a shipping; fallo de cálculo degrada a 0 (no bloquea checkout).
|
||
|
||
## Admin
|
||
- Editor de producto (Prices & Stock): campos "Peso unitario (kg)" y "Compra mínima (uds.)" con guardado por blur/Enter, validación local y mensaje ✓/Error.
|
||
- Gestión de envíos: columna "Peso" con `Máx X kg` y `gratis hasta Y kg`; formulario de método con ambos inputs (acepta coma decimal).
|
||
- `api-client.ts`: firmas de `shippingApi.createMethod/updateMethod` ampliadas (evita la regresión F-085/F-088).
|
||
|
||
## Frontend (storefront customer)
|
||
- Ficha de producto: `AddToCartButton` recibe `minPurchaseQty`; añade la cantidad mínima y muestra "Compra mínima: N unidades."
|
||
- Carrito: `changeQuantity` nunca baja de `minPurchaseQty`; botón − deshabilitado en el mínimo con tooltip explicativo; `addItem` conserva el máximo de los mínimos al fusionar líneas.
|
||
- `types/api.ts`: `Product.unitWeightKg` y `minPurchaseQty`.
|
||
|
||
## Tests
|
||
- `shipping-service.test.ts`: método excluido cuando el peso supera su max; envío gratis denegado cuando el peso supera el rango gratuito.
|
||
- Fixtures de catalog actualizados con los nuevos campos obligatorios.
|
||
|
||
## Evidencia
|
||
- `npm run typecheck` (backend) OK; `npx tsc --noEmit` en apps/admin y frontend OK.
|
||
- `npm test`: 42 archivos, 135 tests passed | 0 failed.
|
||
- Migración 038 aplicada (`db:status`).
|