feat(cotizador): multi-desarrollo — ATELIER, ESTANZA, SENDA, SOLANA + DIAGONAL
- Motor de cálculo parametrizado por desarrollo (enganches, descuentos, splits, fuente de N, quirk Infonavit-lista); DIAGONAL byte-idéntico - 285 unidades (5 desarrollos) en seeds, API (?desarrollo=) y board Monday - Selector de desarrollo en UI, propuestas por config, PDF multi-desarrollo - Knowledge IA: catálogo por desarrollo + matriz de diferencias (10 docs) - Tests: cotizador 53, api 99 — fixtures de los Excel validados al centavo
This commit is contained in:
189
api-gateway/scripts/importar-unidades.ts
Normal file
189
api-gateway/scripts/importar-unidades.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Importa los inventarios de ATELIER, ESTANZA, SENDA y SOLANA al board
|
||||
* "Unidades" de Monday, desde reference/cotizadores/{DEV}.json.
|
||||
*
|
||||
* - Idempotente: salta items cuyo nombre (código prefijado) ya existe.
|
||||
* - Crea la columna "Meses para firma" (numbers) si falta (la usa SOLANA).
|
||||
* - create_labels_if_missing: la columna status "Disponibilidad" gana las
|
||||
* etiquetas nuevas (VENDIDO, ESCRITURADO, APARTADO, BLOQUEADO).
|
||||
* - Delay entre mutaciones para cuidar el rate limit.
|
||||
*
|
||||
* Uso: npx tsx scripts/importar-unidades.ts (desde api-gateway/)
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { config } from '../src/config/index.js'
|
||||
import { MondayService } from '../src/services/monday.service.js'
|
||||
|
||||
const REF_DIR = path.resolve(process.cwd(), '..', 'reference', 'cotizadores')
|
||||
const DELAY_MS = 400
|
||||
|
||||
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
interface UnidadJson {
|
||||
codigo: string
|
||||
modelo: string | null
|
||||
fechaEntrega: string | null
|
||||
supEstacionamiento: number | null
|
||||
supConstruccion: number | null
|
||||
supExcedente: number | null
|
||||
status: string
|
||||
extras: Record<string, string | number | null>
|
||||
}
|
||||
|
||||
interface DefinicionDesarrollo {
|
||||
id: string
|
||||
archivo: string
|
||||
codigoMonday: (u: UnidadJson) => string
|
||||
precio: (u: UnidadJson) => number | null
|
||||
mesesParaFirma?: (u: UnidadJson) => number | null
|
||||
}
|
||||
|
||||
const DESARROLLOS: DefinicionDesarrollo[] = [
|
||||
{
|
||||
id: 'ATELIER',
|
||||
archivo: 'ATELIER.json',
|
||||
codigoMonday: (u) => `ATELIER-${u.codigo}`,
|
||||
precio: (u) => (typeof u.extras['precioLista'] === 'number' ? u.extras['precioLista'] : null),
|
||||
},
|
||||
{
|
||||
id: 'ESTANZA',
|
||||
archivo: 'ESTANZA.json',
|
||||
codigoMonday: (u) => `ESTANZA-${u.codigo.replace(/^CASA-/, '')}`,
|
||||
precio: (u) => (typeof u.extras['precioLista'] === 'number' ? u.extras['precioLista'] : null),
|
||||
},
|
||||
{
|
||||
id: 'SENDA',
|
||||
archivo: 'SENDA.json',
|
||||
codigoMonday: (u) => `SENDA-${u.codigo}`,
|
||||
precio: (u) => (typeof u.extras['precioPreventa'] === 'number' ? u.extras['precioPreventa'] : null),
|
||||
},
|
||||
{
|
||||
id: 'SOLANA',
|
||||
archivo: 'SOLANA.json',
|
||||
codigoMonday: (u) => `SOLANA-${u.codigo}`,
|
||||
precio: (u) => (typeof u.extras['precioPreventa'] === 'number' ? u.extras['precioPreventa'] : null),
|
||||
mesesParaFirma: (u) =>
|
||||
typeof u.extras['mesesParaFirma'] === 'number' ? u.extras['mesesParaFirma'] : null,
|
||||
},
|
||||
]
|
||||
|
||||
const svc = new MondayService()
|
||||
const boardId = config.monday.boards.unidades
|
||||
|
||||
/** Columnas del board mapeadas por título */
|
||||
async function columnasBoard(): Promise<Map<string, { id: string; title: string; type: string }>> {
|
||||
const data = await svc.graphql<{ boards: { columns: { id: string; title: string; type: string }[] }[] }>(
|
||||
`query ($ids: [ID!]) { boards(ids: $ids) { columns { id title type } } }`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
return new Map((data.boards[0]?.columns ?? []).map((c) => [c.title, c]))
|
||||
}
|
||||
|
||||
async function asegurarColumnaMeses(): Promise<void> {
|
||||
const cols = await columnasBoard()
|
||||
if (cols.has('Meses para firma')) return
|
||||
const data = await svc.graphql<{ create_column: { id: string } }>(
|
||||
`mutation ($boardId: ID!, $title: String!) {
|
||||
create_column(board_id: $boardId, title: $title, column_type: numbers) { id }
|
||||
}`,
|
||||
{ boardId, title: 'Meses para firma' },
|
||||
)
|
||||
console.log(`Columna "Meses para firma" creada (${data.create_column.id})`)
|
||||
}
|
||||
|
||||
/** Nombres de TODOS los items del board (paginado por cursor) */
|
||||
async function itemsExistentes(): Promise<Set<string>> {
|
||||
const nombres = new Set<string>()
|
||||
let cursor: string | null = null
|
||||
do {
|
||||
const data: { boards: { items_page: { cursor: string | null; items: { name: string }[] } }[] } =
|
||||
cursor === null
|
||||
? await svc.graphql(
|
||||
`query ($ids: [ID!]) { boards(ids: $ids) { items_page(limit: 500) { cursor items { name } } } }`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
: await svc.graphql(
|
||||
`query ($ids: [ID!], $cursor: String!) {
|
||||
boards(ids: $ids) { items_page(limit: 500, cursor: $cursor) { cursor items { name } } }
|
||||
}`,
|
||||
{ ids: [boardId], cursor },
|
||||
)
|
||||
const page = data.boards[0]?.items_page
|
||||
if (!page) break
|
||||
for (const item of page.items) nombres.add(item.name)
|
||||
cursor = page.cursor
|
||||
} while (cursor !== null)
|
||||
return nombres
|
||||
}
|
||||
|
||||
function columnValuesDe(
|
||||
def: DefinicionDesarrollo,
|
||||
u: UnidadJson,
|
||||
cols: Map<string, { id: string; title: string; type: string }>,
|
||||
): string {
|
||||
const valores: Record<string, unknown> = {}
|
||||
const poner = (titulo: string, valor: unknown) => {
|
||||
const col = cols.get(titulo)
|
||||
if (!col) throw new Error(`Columna "${titulo}" no existe en el board`)
|
||||
valores[col.id] = valor
|
||||
}
|
||||
const precio = def.precio(u)
|
||||
poner('Desarrollo', def.id)
|
||||
poner('Modelo', u.modelo ?? '')
|
||||
if (u.fechaEntrega) poner('Fecha entrega', { date: u.fechaEntrega })
|
||||
if (u.supConstruccion !== null) poner('Sup. construcción', u.supConstruccion)
|
||||
if (u.supExcedente !== null) poner('Sup. excedente', u.supExcedente)
|
||||
if (u.supEstacionamiento !== null) poner('Sup. estacionamiento', u.supEstacionamiento)
|
||||
if (precio !== null) {
|
||||
// Desarrollos de precio único: ambos precios replican el precio de lista
|
||||
poner('Precio sin acabados', precio)
|
||||
poner('Precio con acabados', precio)
|
||||
}
|
||||
poner('Disponibilidad', { label: u.status })
|
||||
const meses = def.mesesParaFirma?.(u)
|
||||
if (meses !== null && meses !== undefined) poner('Meses para firma', meses)
|
||||
return JSON.stringify(valores)
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await asegurarColumnaMeses()
|
||||
const cols = await columnasBoard()
|
||||
const existentes = await itemsExistentes()
|
||||
console.log(`Board ${boardId}: ${existentes.size} items existentes`)
|
||||
|
||||
for (const def of DESARROLLOS) {
|
||||
const raw = JSON.parse(await readFile(path.join(REF_DIR, def.archivo), 'utf8')) as {
|
||||
unidades: UnidadJson[]
|
||||
}
|
||||
let creados = 0
|
||||
let saltados = 0
|
||||
for (const u of raw.unidades) {
|
||||
const nombre = def.codigoMonday(u)
|
||||
if (existentes.has(nombre)) {
|
||||
saltados++
|
||||
continue
|
||||
}
|
||||
const columnValues = columnValuesDe(def, u, cols)
|
||||
await svc.graphql<{ create_item: { id: string } }>(
|
||||
`mutation ($boardId: ID!, $itemName: String!, $columnValues: JSON!) {
|
||||
create_item(board_id: $boardId, item_name: $itemName, column_values: $columnValues, create_labels_if_missing: true) { id }
|
||||
}`,
|
||||
{ boardId, itemName: nombre, columnValues },
|
||||
)
|
||||
existentes.add(nombre)
|
||||
creados++
|
||||
if (creados % 25 === 0) console.log(` ${def.id}: ${creados} creados…`)
|
||||
await sleep(DELAY_MS)
|
||||
}
|
||||
console.log(`${def.id}: ${creados} creados, ${saltados} ya existían (total ${raw.unidades.length})`)
|
||||
}
|
||||
svc.invalidarCacheUnidades()
|
||||
console.log('Importación terminada.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Falló la importación:', err instanceof Error ? err.message : err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user