Sumar el delivery de productos por vendedor🛍
Published: 2020-11-10 02:39:44

Recientemente me pidieron en una codebase qué mantengo hacer un carrito de compras donde el precio del envío sea el precio de envío más caro por productos de un mismo vendedor + el precio más caro de los otros vendedores.

Ejemplo, si en el carrito hay 4 productos, y 3 son de 1 vendedor, calcular el delivery mas costoso de ese vendedor + el delivery del otro vendedor.

La categoría de delivery puede ser 
`moto | auto | van`

El precio de **auto y van** para este uso es el **mismo**.

##Codigo

### Este es un vistaso a la composición final👀
```
const composeProcess = compose(
    reduce((a, b: number) => a + b, 0),
    map(convertToMoney),
    values,
    mapObjIndexed(mapOverProducts),
    addNoise,
    groupByCategory(category)
  )
```

El primer paso es agrupar los productos por categoría, en este caso la categoría será `sellerID`

```
const groupByCategory = (category: string) => (products: { item: Product }[]) => {
    return products.reduce((acc, { item }) => {
      return (acc = { ...acc, [item[category]]: [...(acc[item[category]] || []), item] })
    }, {})
  }
```

Esta función nos va a devolver un árbol de vendedores con productos


**Esta base de datos solo tiene un vendedor, entonces agregaremos un poco de ruido para probar esta función**

## Agregar ruido 🔈
```
 const addNoise = (nodes) => ({
    ...nodes,
    bbc: [
      { id: 10, title: 'jaja', delivery: 'moto' },
      { id: 20, title: 'jeje', delivery: 'auto' },
    ],
    ccc: [{ id: 14, title: 'jiji', delivery: 'auto' }],
  })
```

# Map sobre los productos de cada vendedor 🛍

Lo que nos importa de toda esta función es calcular el delivery más costoso de cada categoria/nodo/vendedor

La siguiente linea es una composición de 3 funciones para conseguir ese delivery más costoso.

`mapObjIndexed(mapOverProducts)`

Donde mapOverProducts:
```
  const mapOverProducts = compose(
    reduce(deliveryReducer, 0), 
    setOf, 
    map(mapOverCategory('delivery'))

  )
```

#### mapOverCategory 
```
  const mapOverCategory = (category: string) => (arr) => arr[category]
```

mapOverCategory('delivery') va a devolver [moto, auto, ...NDeliveries] por cada vendedor

### setOf 
va a devolver el mismo arreglo quitando los deliveries repetidos

`const setOf = (values) => [...new Set(values)]`


### reduce(deliveryReducer, 0) ➕

Ok aquí tenemos 3 escenarios posibles, entonces por comodidad lo manejé con un reducer.

```
const deliveryReducer = (state, action) => {
    switch (action) {
      case 'moto':
        return stateOverNumber(1)(state)

      case 'auto':
        return stateOverNumber(2)(state)

      case 'van':
        return stateOverNumber(3)(state)

      default:
        return state
    }
  }
```

#### Donde stateOverNumber(n)(state) ❔
```
  const stateOverNumber = (num: number) => (state: number) => 
    (state < num ? num : state)

```
***Esta*** es la pieza clave que da categoría a cada producto, lo demás fue llegar aquí.


En este punto podemos regresar a la primera función compuesta, ya terminamos de mapear sobre los productos de cada nodo/vendedor


Recordando que viene *values*
```
const composeProcess = compose(
    reduce((a, b: number) => a + b, 0),
    map(convertToMoney),
    values,
    mapObjIndexed(mapOverProducts),
    addNoise,
    groupByCategory(category)
  )
```

## Values
Ya no necesitamos el id del vendedor, solo sus valores.

## map(converToMoney) 💰

```
const convertToMoney = (deliveryTypes: number) => {
    if (deliveryTypes === 1) {
      return 2.9
    } else {
      return 4.9
    }
  }
```

1 siendo moto, y por el momento los otros valores tienen el mismo precio

### reduce((a, b: number) => a + b, 0) ➕

La última función nos da la suma de estos valores para tener la suma del delivery más costoso de cada vendedor 😁


# Listo, vamos a hacer una abstracción

Ok spoiler alert: uso react. ⚛️

voy a abstraer toda esta lógica en un custom Hook que solo tome la categoría y sobre la cual queremos agrupar los productos

```
const useAbstraction = (category: string) => {
  const groupByCategory = (category: string) => (products: { item: Product }[]) => {
    return products.reduce((acc, { item }) => {
      return (acc = { ...acc, [item[category]]: [...(acc[item[category]] || []), item] })
    }, {})
  }

  const mapOverCategory = (category: string) => (arr) => arr[category]
  const setOf = (values) => [...new Set(values)]

  const stateOverNumber = (num: number) => (state: number) => (state < num ? num : state)

  const deliveryReducer = (state, action) => {
    switch (action) {
      case 'moto':
        return stateOverNumber(1)(state)

      case 'auto':
        return stateOverNumber(2)(state)

      case 'van':
        return stateOverNumber(3)(state)

      default:
        return state
    }
  }
  const mapOverProducts = compose(
    reduce(deliveryReducer, 0),
    setOf,
    map(mapOverCategory('delivery'))
  )
  const addNoise = (sellers) => ({
    ...sellers,
    bbc: [
      { id: 10, title: 'jaja', delivery: 'moto' },
      { id: 20, title: 'jeje', delivery: 'auto' },
    ],
    ccc: [{ id: 14, title: 'jiji', delivery: 'auto' }],
  })

  const convertToMoney = (deliveryTypes: number) => {
    if (deliveryTypes === 1) {
      return 2.9
    } else {
      return 4.9
    }
  }

  const composeProcess = compose(
    reduce((a, b: number) => a + b, 0),
    map(convertToMoney),
    values,
    mapObjIndexed(mapOverProducts),
    addNoise,
    groupByCategory(category)
  )

  return { composeProcess }
}
```

***Y ahora solo usamos***

```
  const { composeProcess } = useAbstraction('sellerID')
  const delivery = useMemo( () 
   => composeProcess(state.products)
   , [state.products])
```

### Outro

![Stonks](https://i.kym-cdn.com/photos/images/newsfeed/001/499/826/2f0.png)
Probablemente pueda mejorar el reducer, pero ya esto soporta la posibilidad de agrupar en un futuro por ciudad o estado.

Mi nombre es Jesus Cova, trabajo con el equipo de [midnightouizard](https://www.instagram.com/midnightouizard/)🧙🏼‍♂️ y me pueden contactar en [twitter](https://twitter.com/jesusacova) o en jesuscovam@gmail.com

## ¡Gracias por tu tiempo!