- Cotizador (/quote): 4 propuestas con formulas validadas del Excel, comparador, combobox de unidades, vigencia, PDF ANEXO A - Alta Clientes (/client): wizard 8 pasos, validacion CURP RENAPO, OCR de INE, checklist de expediente, firma digital, 5 PDFs legales - Cobranza (/pay): planes de enganche, mora, vista cobranza del dia, recibos PDF, bitacora, dashboard con filtros - Landing (/): entrada al ecosistema - Design system StudioBlank (monocromo, acento #1B4332) en las 4 apps - 123 tests vitest
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
import type { PlanPago } from '../types'
|
|
import { generarSeed } from '../data/seed'
|
|
import { generarCalendario, registrarAbono, statusPlan, agregarNotaAPlan, type AbonoInput } from '../lib/cobranza'
|
|
import { uuid } from '../lib/uuid'
|
|
|
|
export const CLAVE_STORAGE = 'frasa_cobranza_v1'
|
|
|
|
// Inyectable para pruebas (stub en vitest, donde no hay localStorage)
|
|
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>
|
|
|
|
function storageDefault(): StorageLike {
|
|
return globalThis.localStorage
|
|
}
|
|
|
|
/**
|
|
* Carga los planes desde localStorage; si está vacío siembra los datos de ejemplo.
|
|
* Migra planes guardados con versiones anteriores del modelo (sin `notas`).
|
|
* TODO: reemplazar por GET /api/planes cuando el API Gateway esté disponible.
|
|
*/
|
|
export function cargarPlanes(storage: StorageLike = storageDefault()): PlanPago[] {
|
|
const crudo = storage.getItem(CLAVE_STORAGE)
|
|
if (crudo) {
|
|
const planes = JSON.parse(crudo) as PlanPago[]
|
|
return planes.map((p) => (p.notas ? p : { ...p, notas: [] }))
|
|
}
|
|
const planes = generarSeed()
|
|
guardarPlanes(planes, storage)
|
|
return planes
|
|
}
|
|
|
|
export function guardarPlanes(planes: PlanPago[], storage: StorageLike = storageDefault()): void {
|
|
storage.setItem(CLAVE_STORAGE, JSON.stringify(planes))
|
|
}
|
|
|
|
export function obtenerPlan(id: string, storage: StorageLike = storageDefault()): PlanPago | undefined {
|
|
return cargarPlanes(storage).find((p) => p.id === id)
|
|
}
|
|
|
|
export interface NuevoPlanInput {
|
|
clienteNombre: string
|
|
clienteTelefono?: string
|
|
unidadCodigo: string
|
|
proyecto: string
|
|
ventaRef: string
|
|
fechaInicio: string
|
|
engancheTotal: number
|
|
numPagos: number
|
|
tasaMoraMensual?: number
|
|
}
|
|
|
|
export function crearPlan(input: NuevoPlanInput, storage: StorageLike = storageDefault()): PlanPago {
|
|
const { mensualidad, pagos } = generarCalendario(input.fechaInicio, input.engancheTotal, input.numPagos)
|
|
const plan: PlanPago = {
|
|
id: uuid(),
|
|
ventaRef: input.ventaRef,
|
|
clienteNombre: input.clienteNombre,
|
|
clienteTelefono: input.clienteTelefono || undefined,
|
|
unidadCodigo: input.unidadCodigo,
|
|
proyecto: input.proyecto,
|
|
fechaInicio: input.fechaInicio,
|
|
engancheTotal: input.engancheTotal,
|
|
numPagos: input.numPagos,
|
|
mensualidad,
|
|
tasaMoraMensual: input.tasaMoraMensual,
|
|
status: 'ACTIVO',
|
|
pagos,
|
|
notas: [],
|
|
createdAt: new Date().toISOString(),
|
|
}
|
|
guardarPlanes([...cargarPlanes(storage), plan], storage)
|
|
return plan
|
|
}
|
|
|
|
export function actualizarPlan(plan: PlanPago, storage: StorageLike = storageDefault()): PlanPago {
|
|
// El status operativo (ACTIVO/LIQUIDADO) se deriva del saldo; CANCELADO es manual
|
|
const actualizado: PlanPago = { ...plan, status: statusPlan(plan) }
|
|
guardarPlanes(
|
|
cargarPlanes(storage).map((p) => (p.id === actualizado.id ? actualizado : p)),
|
|
storage,
|
|
)
|
|
return actualizado
|
|
}
|
|
|
|
export function cancelarPlan(id: string, storage: StorageLike = storageDefault()): PlanPago {
|
|
const plan = obtenerPlan(id, storage)
|
|
if (!plan) throw new Error('Plan no encontrado')
|
|
const cancelado: PlanPago = { ...plan, status: 'CANCELADO' }
|
|
guardarPlanes(
|
|
cargarPlanes(storage).map((p) => (p.id === id ? cancelado : p)),
|
|
storage,
|
|
)
|
|
return cancelado
|
|
}
|
|
|
|
export function registrarAbonoEnPlan(
|
|
planId: string,
|
|
abono: Omit<AbonoInput, 'pagoId'>,
|
|
pagoId: string,
|
|
storage: StorageLike = storageDefault(),
|
|
): PlanPago {
|
|
const plan = obtenerPlan(planId, storage)
|
|
if (!plan) throw new Error('Plan no encontrado')
|
|
return actualizarPlan(registrarAbono(plan, { ...abono, pagoId }), storage)
|
|
}
|
|
|
|
/** Agrega una nota a la bitácora del plan y persiste el cambio */
|
|
export function agregarNota(
|
|
planId: string,
|
|
texto: string,
|
|
storage: StorageLike = storageDefault(),
|
|
): PlanPago {
|
|
const plan = obtenerPlan(planId, storage)
|
|
if (!plan) throw new Error('Plan no encontrado')
|
|
return actualizarPlan(agregarNotaAPlan(plan, texto), storage)
|
|
}
|