feat: ecosistema FRASA — apps Cotizador, Alta Clientes, Cobranza y Landing
- 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
This commit is contained in:
115
apps/cobranza/src/services/repository.ts
Normal file
115
apps/cobranza/src/services/repository.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user