almeira-web/facturacion-dian-hub/backend/src/domain/entities/ResolucionDian.ts

103 lines
3.4 KiB
TypeScript

import { TipoDocumentoFiscal } from '../value-objects/FiscalEnums.js';
import { ResolucionAgotadaException } from '../exceptions/ValidacionFiscalException.js';
export interface ResolucionDianProps {
id?: string;
tenantId?: string;
resolutionNumber: string;
prefix: string;
fromNumber: bigint;
toNumber: bigint;
currentConsecutive: bigint;
technicalKey: string;
startDate: Date;
endDate: Date;
isActive?: boolean;
documentType?: TipoDocumentoFiscal;
}
export class ResolucionDian {
public readonly id?: string;
public readonly tenantId?: string;
public readonly resolutionNumber: string;
public readonly prefix: string;
public readonly fromNumber: bigint;
public readonly toNumber: bigint;
private _currentConsecutive: bigint;
public readonly technicalKey: string;
public readonly startDate: Date;
public readonly endDate: Date;
public readonly isActive: boolean;
public readonly documentType: TipoDocumentoFiscal;
constructor(props: ResolucionDianProps) {
this.id = props.id;
this.tenantId = props.tenantId;
this.resolutionNumber = props.resolutionNumber.trim();
this.prefix = props.prefix.trim().toUpperCase();
this.fromNumber = BigInt(props.fromNumber);
this.toNumber = BigInt(props.toNumber);
this._currentConsecutive = BigInt(props.currentConsecutive);
this.technicalKey = props.technicalKey.trim();
this.startDate = props.startDate;
this.endDate = props.endDate;
this.isActive = props.isActive ?? true;
this.documentType = props.documentType || TipoDocumentoFiscal.FACTURA_VENTA;
this.validarInvariantes();
}
private validarInvariantes(): void {
if (this.fromNumber > this.toNumber) {
throw new Error('El rango inicial no puede ser mayor que el rango final de la resolución.');
}
}
public get currentConsecutive(): bigint {
return this._currentConsecutive;
}
public estaVigente(fechaReferencia: Date = new Date()): boolean {
if (!this.isActive) return false;
return fechaReferencia >= this.startDate && fechaReferencia <= this.endDate;
}
public tieneNumerosDisponibles(): boolean {
return this._currentConsecutive < this.toNumber;
}
public obtenerSiguienteConsecutivo(): bigint {
if (!this.estaVigente()) {
throw new ResolucionAgotadaException(
`La resolución ${this.resolutionNumber} (Prefijo: ${this.prefix}) ha expirado o se encuentra inactiva.`
);
}
if (!this.tieneNumerosDisponibles()) {
throw new ResolucionAgotadaException(
`Rango de numeración agotado para la resolución ${this.resolutionNumber} (Prefijo: ${this.prefix}). Rango límite: ${this.toNumber}`
);
}
this._currentConsecutive += 1n;
return this._currentConsecutive;
}
public formatearNumeroCompleto(consecutivo: bigint): string {
return this.prefix ? `${this.prefix}-${consecutivo}` : `${consecutivo}`;
}
public porcentajeConsumido(): number {
const total = Number(this.toNumber - this.fromNumber + 1n);
if (total <= 0) return 100;
const usados = Number(this._currentConsecutive - this.fromNumber + 1n);
const porcentaje = Math.min(100, Math.max(0, (usados / total) * 100));
return Math.round(porcentaje * 10) / 10;
}
public diasRestantesVigencia(ahora: Date = new Date()): number {
const diffMs = this.endDate.getTime() - ahora.getTime();
return Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
}
}