Hace dos meses, Carlos —dueño de 3 tiendas de ropa en Providencia— me contactó porque estaba perdiendo 12 horas a la semana haciendo reportes de inventario a mano. Sus vendedoras le mandaban Excel por WhatsApp, él los consolidaba en la madrugada, y siempre había diferencias que no cuadraban. La paja.
El problema
Cada tienda usaba Bsale como POS, pero Carlos no tenía visibilidad en tiempo real del stock entre las 3 sucursales. Los problemas eran concretos:
- Productos que se agotaban sin aviso (perdía ventas)
- Reportes manuales que tomaban 4 horas cada lunes
- Diferencias de inventario que aparecían semanas después
- Transferencias entre tiendas coordinadas por WhatsApp
- Cero visibilidad de productos de baja rotación
En enero perdió $800.000 CLP porque no sabía que se habían agotado las poleras más vendidas en la tienda de Las Condes, mientras en Providencia tenía 50 unidades sin moverse.
Cómo funciona
Buildeé un sistema que conecta directamente con la API de Bsale y procesa cada venta en tiempo real. Nada de imports manuales ni Excel.
Flujo del sistema de inventario automatizado
El flujo es simple: cada vez que se registra una venta en cualquier POS, Bsale dispara un webhook. Mi sistema valida el stock, actualiza la base de datos y, si algo está bajo el mínimo, manda alertas automáticas por WhatsApp.
Paso a paso
Conexión con Bsale API
Primero configuré la integración con Bsale. Su API es decente, aunque la documentación tiene algunos vacíos.
// lib/bsale-client.ts
export class BsaleClient {
private baseUrl = 'https://api.bsale.io/v1'
private token: string
constructor(token: string) {
this.token = token
}
async getProducts(storeId: number) {
const response = await fetch(`${this.baseUrl}/products.json?office=${storeId}`, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`Bsale API error: ${response.status}`)
}
return response.json()
}
async getStock(productId: number) {
const response = await fetch(`${this.baseUrl}/stocks.json?variant=${productId}`)
return response.json()
}
}Webhook Handler para ventas en tiempo real
Lo más crítico era procesar las ventas al instante. Bsale manda un webhook cada vez que se completa una transacción.
// app/api/webhooks/bsale/route.ts
import { NextRequest } from 'next/server'
import { updateInventory } from '@/lib/inventory'
import { sendStockAlert } from '@/lib/notifications'
export async function POST(request: NextRequest) {
const payload = await request.json()
if (payload.event === 'document_created' && payload.document.documentType.id === 1) {
// Es una boleta/factura (venta real)
const details = payload.document.details
for (const item of details) {
const newStock = await updateInventory({
productId: item.variant.id,
storeId: payload.document.office.id,
quantity: -item.quantity // Resta del inventario
})
// Alerta si queda poco stock
if (newStock <= item.variant.minimumStock) {
await sendStockAlert({
productName: item.variant.description,
currentStock: newStock,
storeId: payload.document.office.id
})
}
}
}
return Response.json({ received: true })
}Dashboard de control
El dashboard lo hice con Next.js y Chart.js. Carlos quería ver todo de un vistazo: stock actual, ventas del día, productos críticos.
// components/InventoryDashboard.tsx
export default function InventoryDashboard() {
const { data: stores } = useSWR('/api/stores', fetcher)
const { data: lowStock } = useSWR('/api/inventory/low-stock', fetcher)
const { data: todaySales } = useSWR('/api/sales/today', fetcher)
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<StockAlertsCard items={lowStock} />
<SalesTodayCard sales={todaySales} />
<StoreComparisonCard stores={stores} />
<div className="lg:col-span-3">
<ProductMovementChart />
</div>
</div>
)
}Reportes automáticos
Configué un cron job que corre todos los días a las 6 AM y genera el reporte consolidado. Se manda por email y queda guardado en el sistema.
// lib/cron-jobs.ts
export async function generateDailyReport() {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
const report = await generateReport({
startDate: yesterday,
endDate: yesterday,
includeAllStores: true
})
await sendReportEmail({
to: process.env.OWNER_EMAIL,
subject: `Reporte diario - ${format(yesterday, 'dd/MM/yyyy')}`,
reportData: report
})
console.log(`Daily report sent for ${format(yesterday, 'dd/MM/yyyy')}`)
}Resultados
Después de 6 semanas en producción, los números hablan solos:
- 40% menos tiempo en reportes: de 12 horas semanales a 7 horas
- 85% menos errores de inventario (antes 15-20 diferencias por semana, ahora 2-3)
- $1.2M CLP extra en ventas por mejor distribución de stock entre tiendas
- Alertas en 30 segundos cuando un producto se agota
- 100% automatización en reportes diarios
Carlos ahora ve el stock de las 3 tiendas desde el celular y sabe al instante cuándo transferir productos entre sucursales.
Lo que aprendí
- Los webhooks de Bsale a veces llegan duplicados. Tuve que implementar idempotencia con una tabla de eventos procesados.
- La API tiene rate limits no documentados. Después de 100 requests por minuto empieza a devolver 429. Implementé un sistema de cola con Redis.
- Las validaciones de stock son críticas. Un bug me dejó inventario negativo durante 3 días hasta que Carlos me avisó.
- WhatsApp Business API vale cada peso. Las notificaciones por email se perdían, pero los WhatsApp los leen al toque.
- El dashboard mobile es fundamental. Carlos revisa el inventario desde el auto entre tiendas, no desde el computador.
Para cerrar
Lo que más me sorprendió fue ver cómo un sistema relativamente simple puede cambiar completamente la operación de un negocio. Carlos pasó de estar estresado con Excel a tener control total de su inventario.
El stack completo está corriendo en un VPS de Hetzner por €20 al mes. Nada fancy, pero funciona perfecto para el volumen que manejan.
¿Tienes algún proceso manual que te está consumiendo horas? Me encantaría escuchar qué estás automatizando en tu negocio.
