Refactor: Mejora la lógica de facturación y la UI
Este commit introduce una refactorización significativa en el módulo de
suscripciones para alinear el sistema con reglas de negocio clave:
facturación consolidada por empresa, cobro a mes adelantado con
imputación de ajustes diferida, y una interfaz de usuario más clara.
Backend:
- **Facturación por Empresa:** Se modifica `FacturacionService` para
agrupar las suscripciones por cliente y empresa, generando una
factura consolidada para cada combinación. Esto asegura la correcta
separación fiscal.
- **Imputación de Ajustes:** Se ajusta la lógica para que la facturación
de un período (ej. Septiembre) aplique únicamente los ajustes
pendientes cuya fecha corresponde al período anterior (Agosto).
- **Cierre Secuencial:** Se implementa una validación en
`GenerarFacturacionMensual` que impide generar la facturación de un
período si el anterior no ha sido cerrado, garantizando el orden
cronológico.
- **Emails Consolidados:** El proceso de notificación automática al
generar el cierre ahora envía un único email consolidado por
suscriptor, detallando los cargos de todas sus facturas/empresas.
- **Envío de PDF Individual:** Se refactoriza el endpoint de envío manual
para que opere sobre una `idFactura` individual y adjunte el PDF
correspondiente si existe.
- **Repositorios Mejorados:** Se optimizan y añaden métodos en
`FacturaRepository` y `AjusteRepository` para soportar los nuevos
requisitos de filtrado y consulta de datos consolidados.
Frontend:
- **Separación de Vistas:** La página de "Facturación" se divide en dos:
- `ProcesosPage`: Para acciones masivas (generar cierre, archivo de
débito, procesar respuesta).
- `ConsultaFacturasPage`: Una nueva página dedicada a buscar,
filtrar y gestionar facturas individuales con una interfaz de doble
acordeón (Suscriptor -> Empresa).
- **Filtros Avanzados:** La página `ConsultaFacturasPage` ahora incluye
filtros por nombre de suscriptor, estado de pago y estado de
facturación.
- **Filtros de Fecha por Defecto:** La página de "Cuenta Corriente"
ahora filtra por el mes actual por defecto para mejorar el rendimiento
y la usabilidad.
- **Validación de Fechas:** Se añade lógica en los filtros de fecha para
impedir la selección de rangos inválidos.
- **Validación de Monto de Pago:** El modal de pago manual ahora impide
registrar un monto superior al saldo pendiente de la factura.
This commit is contained in:
@@ -445,6 +445,25 @@ const getListadoDistMensualPorPublicacionPdf = async (params: GetListadoDistMens
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getReporteFacturasPublicidadPdf = async (anio: number, mes: number): Promise<{ fileContent: Blob, fileName: string }> => {
|
||||
const params = new URLSearchParams({ anio: String(anio), mes: String(mes) });
|
||||
const url = `/reportes/suscripciones/facturas-para-publicidad/pdf?${params.toString()}`;
|
||||
const response = await apiClient.get(url, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
let fileName = `ReportePublicidad_Suscripciones_${anio}-${String(mes).padStart(2, '0')}.pdf`; // Fallback
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||
if (fileNameMatch && fileNameMatch.length > 1) {
|
||||
fileName = fileNameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
return { fileContent: response.data, fileName: fileName };
|
||||
};
|
||||
|
||||
const reportesService = {
|
||||
getExistenciaPapel,
|
||||
getExistenciaPapelPdf,
|
||||
@@ -487,7 +506,8 @@ const reportesService = {
|
||||
getListadoDistMensualDiarios,
|
||||
getListadoDistMensualDiariosPdf,
|
||||
getListadoDistMensualPorPublicacion,
|
||||
getListadoDistMensualPorPublicacionPdf,
|
||||
getListadoDistMensualPorPublicacionPdf,
|
||||
getReporteFacturasPublicidadPdf,
|
||||
};
|
||||
|
||||
export default reportesService;
|
||||
@@ -1,12 +1,26 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { AjusteDto } from '../../models/dtos/Suscripciones/AjusteDto';
|
||||
import type { CreateAjusteDto } from '../../models/dtos/Suscripciones/CreateAjusteDto';
|
||||
import type { UpdateAjusteDto } from '../../models/dtos/Suscripciones/UpdateAjusteDto';
|
||||
|
||||
const API_URL_BY_SUSCRIPTOR = '/suscriptores';
|
||||
const API_URL_BASE = '/ajustes';
|
||||
|
||||
const getAjustesPorSuscriptor = async (idSuscriptor: number): Promise<AjusteDto[]> => {
|
||||
const response = await apiClient.get<AjusteDto[]>(`${API_URL_BY_SUSCRIPTOR}/${idSuscriptor}/ajustes`);
|
||||
const getAjustesPorSuscriptor = async (idSuscriptor: number, fechaDesde?: string, fechaHasta?: string): Promise<AjusteDto[]> => {
|
||||
// URLSearchParams nos ayuda a construir la query string de forma segura y limpia
|
||||
const params = new URLSearchParams();
|
||||
if (fechaDesde) {
|
||||
params.append('fechaDesde', fechaDesde);
|
||||
}
|
||||
if (fechaHasta) {
|
||||
params.append('fechaHasta', fechaHasta);
|
||||
}
|
||||
|
||||
// Si hay parámetros, los añadimos a la URL. Si no, la URL queda limpia.
|
||||
const queryString = params.toString();
|
||||
const url = `${API_URL_BY_SUSCRIPTOR}/${idSuscriptor}/ajustes${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await apiClient.get<AjusteDto[]>(url);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -19,8 +33,13 @@ const anularAjuste = async (idAjuste: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL_BASE}/${idAjuste}/anular`);
|
||||
};
|
||||
|
||||
const updateAjuste = async (idAjuste: number, data: UpdateAjusteDto): Promise<void> => {
|
||||
await apiClient.put(`${API_URL_BASE}/${idAjuste}`, data);
|
||||
};
|
||||
|
||||
export default {
|
||||
getAjustesPorSuscriptor,
|
||||
createAjusteManual,
|
||||
anularAjuste
|
||||
anularAjuste,
|
||||
updateAjuste,
|
||||
};
|
||||
@@ -1,29 +1,33 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { FacturaDto } from '../../models/dtos/Suscripciones/FacturaDto';
|
||||
import type { GenerarFacturacionResponseDto } from '../../models/dtos/Suscripciones/GenerarFacturacionResponseDto';
|
||||
import type { PagoDto } from '../../models/dtos/Suscripciones/PagoDto';
|
||||
import type { CreatePagoDto } from '../../models/dtos/Suscripciones/CreatePagoDto';
|
||||
import type { ProcesamientoLoteResponseDto } from '../../models/dtos/Suscripciones/ProcesamientoLoteResponseDto';
|
||||
import type { ResumenCuentaSuscriptorDto } from '../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
|
||||
|
||||
const API_URL = '/facturacion';
|
||||
const DEBITOS_URL = '/debitos';
|
||||
const PAGOS_URL = '/pagos';
|
||||
const FACTURAS_URL = '/facturas';
|
||||
|
||||
const procesarArchivoRespuesta = async (archivo: File): Promise<ProcesamientoLoteResponseDto> => {
|
||||
const formData = new FormData();
|
||||
formData.append('archivo', archivo);
|
||||
|
||||
const response = await apiClient.post<ProcesamientoLoteResponseDto>(`${DEBITOS_URL}/procesar-respuesta`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getFacturasPorPeriodo = async (anio: number, mes: number): Promise<FacturaDto[]> => {
|
||||
const response = await apiClient.get<FacturaDto[]>(`${API_URL}/${anio}/${mes}`);
|
||||
const getResumenesDeCuentaPorPeriodo = async (anio: number, mes: number, nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string): Promise<ResumenCuentaSuscriptorDto[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor);
|
||||
if (estadoPago) params.append('estadoPago', estadoPago);
|
||||
if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await apiClient.get<ResumenCuentaSuscriptorDto[]>(url);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -36,7 +40,6 @@ const generarArchivoDebito = async (anio: number, mes: number): Promise<{ fileCo
|
||||
const response = await apiClient.post(`${DEBITOS_URL}/${anio}/${mes}/generar-archivo`, {}, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
let fileName = `debito_${anio}_${mes}.txt`;
|
||||
if (contentDisposition) {
|
||||
@@ -45,30 +48,35 @@ const generarArchivoDebito = async (anio: number, mes: number): Promise<{ fileCo
|
||||
fileName = fileNameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
return { fileContent: response.data, fileName: fileName };
|
||||
};
|
||||
|
||||
const getPagosPorFactura = async (idFactura: number): Promise<PagoDto[]> => {
|
||||
const response = await apiClient.get<PagoDto[]>(`${FACTURAS_URL}/${idFactura}/pagos`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const registrarPagoManual = async (data: CreatePagoDto): Promise<PagoDto> => {
|
||||
const response = await apiClient.post<PagoDto>(PAGOS_URL, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const enviarFacturaPorEmail = async (idFactura: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL}/${idFactura}/enviar-email`);
|
||||
const actualizarNumeroFactura = async (idFactura: number, numeroFactura: string): Promise<void> => {
|
||||
await apiClient.put(`${API_URL}/${idFactura}/numero-factura`, `"${numeroFactura}"`, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
|
||||
const enviarAvisoCuentaPorEmail = async (anio: number, mes: number, idSuscriptor: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL}/${anio}/${mes}/suscriptor/${idSuscriptor}/enviar-aviso`);
|
||||
};
|
||||
|
||||
const enviarFacturaPdfPorEmail = async (idFactura: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL}/${idFactura}/enviar-factura-pdf`);
|
||||
};
|
||||
|
||||
export default {
|
||||
procesarArchivoRespuesta,
|
||||
getFacturasPorPeriodo,
|
||||
getResumenesDeCuentaPorPeriodo,
|
||||
generarFacturacionMensual,
|
||||
generarArchivoDebito,
|
||||
getPagosPorFactura,
|
||||
registrarPagoManual,
|
||||
enviarFacturaPorEmail,
|
||||
actualizarNumeroFactura,
|
||||
enviarAvisoCuentaPorEmail,
|
||||
enviarFacturaPdfPorEmail,
|
||||
};
|
||||
@@ -3,12 +3,15 @@ import type { SuscripcionDto } from '../../models/dtos/Suscripciones/Suscripcion
|
||||
import type { CreateSuscripcionDto } from '../../models/dtos/Suscripciones/CreateSuscripcionDto';
|
||||
import type { UpdateSuscripcionDto } from '../../models/dtos/Suscripciones/UpdateSuscripcionDto';
|
||||
import type { PromocionDto } from '../../models/dtos/Suscripciones/PromocionDto';
|
||||
import type { PromocionAsignadaDto } from '../../models/dtos/Suscripciones/PromocionAsignadaDto';
|
||||
import type { AsignarPromocionDto } from '../../models/dtos/Suscripciones/AsignarPromocionDto';
|
||||
|
||||
const API_URL_BASE = '/suscripciones';
|
||||
const API_URL_BY_SUSCRIPTOR = '/suscriptores'; // Para la ruta anidada
|
||||
const API_URL_SUSCRIPTORES = '/suscriptores';
|
||||
|
||||
const getSuscripcionesPorSuscriptor = async (idSuscriptor: number): Promise<SuscripcionDto[]> => {
|
||||
const response = await apiClient.get<SuscripcionDto[]>(`${API_URL_BY_SUSCRIPTOR}/${idSuscriptor}/suscripciones`);
|
||||
// La URL correcta es /suscriptores/{id}/suscripciones, no /suscripciones/suscriptor/...
|
||||
const response = await apiClient.get<SuscripcionDto[]>(`${API_URL_SUSCRIPTORES}/${idSuscriptor}/suscripciones`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -26,8 +29,8 @@ const updateSuscripcion = async (id: number, data: UpdateSuscripcionDto): Promis
|
||||
await apiClient.put(`${API_URL_BASE}/${id}`, data);
|
||||
};
|
||||
|
||||
const getPromocionesAsignadas = async (idSuscripcion: number): Promise<PromocionDto[]> => {
|
||||
const response = await apiClient.get<PromocionDto[]>(`${API_URL_BASE}/${idSuscripcion}/promociones`);
|
||||
const getPromocionesAsignadas = async (idSuscripcion: number): Promise<PromocionAsignadaDto[]> => {
|
||||
const response = await apiClient.get<PromocionAsignadaDto[]>(`${API_URL_BASE}/${idSuscripcion}/promociones`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -36,8 +39,8 @@ const getPromocionesDisponibles = async (idSuscripcion: number): Promise<Promoci
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const asignarPromocion = async (idSuscripcion: number, idPromocion: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL_BASE}/${idSuscripcion}/promociones/${idPromocion}`);
|
||||
const asignarPromocion = async (idSuscripcion: number, data: AsignarPromocionDto): Promise<void> => {
|
||||
await apiClient.post(`${API_URL_BASE}/${idSuscripcion}/promociones`, data);
|
||||
};
|
||||
|
||||
const quitarPromocion = async (idSuscripcion: number, idPromocion: number): Promise<void> => {
|
||||
@@ -52,5 +55,5 @@ export default {
|
||||
getPromocionesAsignadas,
|
||||
getPromocionesDisponibles,
|
||||
asignarPromocion,
|
||||
quitarPromocion
|
||||
quitarPromocion,
|
||||
};
|
||||
Reference in New Issue
Block a user