Initial commit - Horux Despachos NL
This commit is contained in:
248
apps/api/src/services/sat/sat-client.service.ts
Normal file
248
apps/api/src/services/sat/sat-client.service.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import {
|
||||
Fiel,
|
||||
HttpsWebClient,
|
||||
FielRequestBuilder,
|
||||
Service,
|
||||
QueryParameters,
|
||||
DateTimePeriod,
|
||||
DownloadType,
|
||||
RequestType,
|
||||
DocumentStatus,
|
||||
ServiceEndpoints,
|
||||
} from '@nodecfdi/sat-ws-descarga-masiva';
|
||||
|
||||
export interface FielData {
|
||||
cerContent: string;
|
||||
keyContent: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea el servicio de descarga masiva del SAT usando los datos de la FIEL
|
||||
*/
|
||||
export function createSatService(fielData: FielData): Service {
|
||||
// Crear FIEL usando el método estático create
|
||||
const fiel = Fiel.create(fielData.cerContent, fielData.keyContent, fielData.password);
|
||||
|
||||
// Verificar que la FIEL sea válida
|
||||
if (!fiel.isValid()) {
|
||||
throw new Error('La FIEL no es válida o está vencida');
|
||||
}
|
||||
|
||||
// Crear cliente HTTP
|
||||
const webClient = new HttpsWebClient();
|
||||
|
||||
// Crear request builder con la FIEL
|
||||
const requestBuilder = new FielRequestBuilder(fiel);
|
||||
|
||||
// Crear y retornar el servicio
|
||||
return new Service(requestBuilder, webClient, undefined, ServiceEndpoints.cfdi());
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
success: boolean;
|
||||
requestId?: string;
|
||||
message: string;
|
||||
statusCode?: string;
|
||||
}
|
||||
|
||||
export interface VerifyResult {
|
||||
success: boolean;
|
||||
status: 'pending' | 'processing' | 'ready' | 'failed' | 'rejected';
|
||||
packageIds: string[];
|
||||
totalCfdis: number;
|
||||
message: string;
|
||||
statusCode?: string;
|
||||
}
|
||||
|
||||
export interface DownloadResult {
|
||||
success: boolean;
|
||||
packageContent: string; // Base64 encoded ZIP
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza una consulta al SAT para solicitar CFDIs
|
||||
*/
|
||||
export async function querySat(
|
||||
service: Service,
|
||||
fechaInicio: Date,
|
||||
fechaFin: Date,
|
||||
tipo: 'emitidos' | 'recibidos',
|
||||
requestType: 'metadata' | 'cfdi' = 'cfdi'
|
||||
): Promise<QueryResult> {
|
||||
try {
|
||||
const period = DateTimePeriod.createFromValues(
|
||||
formatDateForSat(fechaInicio),
|
||||
formatDateForSat(fechaFin)
|
||||
);
|
||||
|
||||
const downloadType = new DownloadType(tipo === 'emitidos' ? 'issued' : 'received');
|
||||
const reqType = new RequestType(requestType === 'cfdi' ? 'xml' : 'metadata');
|
||||
|
||||
// XMLs: solo vigentes (active). Metadata: todos (undefined = sin filtro).
|
||||
let parameters = QueryParameters.create(period, downloadType, reqType);
|
||||
if (requestType === 'cfdi') {
|
||||
parameters = parameters.withDocumentStatus(new DocumentStatus('active'));
|
||||
}
|
||||
const result = await service.query(parameters);
|
||||
|
||||
if (!result.getStatus().isAccepted()) {
|
||||
return {
|
||||
success: false,
|
||||
message: result.getStatus().getMessage(),
|
||||
statusCode: result.getStatus().getCode().toString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
requestId: result.getRequestId(),
|
||||
message: 'Solicitud aceptada',
|
||||
statusCode: result.getStatus().getCode().toString(),
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('[SAT Query Error]', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Error al realizar consulta',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica el estado de una solicitud
|
||||
*/
|
||||
export async function verifySatRequest(
|
||||
service: Service,
|
||||
requestId: string
|
||||
): Promise<VerifyResult> {
|
||||
try {
|
||||
const result = await service.verify(requestId);
|
||||
const statusRequest = result.getStatusRequest();
|
||||
|
||||
// `codeRequest` es el código SAT específico del estado de la solicitud
|
||||
// (5000 Accepted, 5002 Exhausted, 5003 MaximumLimit, 5004 EmptyResult,
|
||||
// 5005 Duplicated) y su mensaje explica POR QUÉ el SAT rechazó. Es la
|
||||
// pieza clave para diagnosticar rejections — el `getStatus().getCode()`
|
||||
// solo devuelve el wrapper HTTP (5000 genérico "Aceptada").
|
||||
//
|
||||
// Fuente: docs phpcfdi + lib @nodecfdi/sat-ws-descarga-masiva (`CodeRequest`).
|
||||
const codeReqObj = typeof (result as any).getCodeRequest === 'function'
|
||||
? (result as any).getCodeRequest()
|
||||
: null;
|
||||
const codeRequestValue = codeReqObj ? codeReqObj.getValue() : null;
|
||||
const codeRequestMessage = codeReqObj ? codeReqObj.getMessage() : null;
|
||||
const codeRequestEntry = codeReqObj ? codeReqObj.getEntryId() : null;
|
||||
|
||||
// Debug logging
|
||||
console.log('[SAT Verify Debug]', {
|
||||
statusRequestValue: statusRequest.getValue(),
|
||||
statusRequestEntryId: statusRequest.getEntryId(),
|
||||
cfdis: result.getNumberCfdis(),
|
||||
packages: result.getPackageIds(),
|
||||
statusCode: result.getStatus().getCode(),
|
||||
statusMsg: result.getStatus().getMessage(),
|
||||
codeRequestValue,
|
||||
codeRequestEntry,
|
||||
codeRequestMessage,
|
||||
});
|
||||
|
||||
// Usar isTypeOf para determinar el estado
|
||||
let status: VerifyResult['status'];
|
||||
if (statusRequest.isTypeOf('Finished')) {
|
||||
status = 'ready';
|
||||
} else if (statusRequest.isTypeOf('InProgress')) {
|
||||
status = 'processing';
|
||||
} else if (statusRequest.isTypeOf('Accepted')) {
|
||||
status = 'pending';
|
||||
} else if (statusRequest.isTypeOf('Failure')) {
|
||||
status = 'failed';
|
||||
} else if (statusRequest.isTypeOf('Rejected')) {
|
||||
status = 'rejected';
|
||||
} else {
|
||||
// Default: check by entryId
|
||||
const entryId = statusRequest.getEntryId();
|
||||
if (entryId === 'Finished') status = 'ready';
|
||||
else if (entryId === 'InProgress') status = 'processing';
|
||||
else if (entryId === 'Accepted') status = 'pending';
|
||||
else status = 'pending';
|
||||
}
|
||||
|
||||
// Para estados terminales no-felices, construir mensaje informativo.
|
||||
// `codeRequest` (si está disponible) es la razón SAT real del rechazo.
|
||||
const statusCode = result.getStatus().getCode().toString();
|
||||
const statusMsg = result.getStatus().getMessage();
|
||||
const reqValue = statusRequest.getValue();
|
||||
const reqEntry = statusRequest.getEntryId();
|
||||
let message = statusMsg;
|
||||
if (status === 'rejected' || status === 'failed') {
|
||||
const codeReqStr = codeRequestValue
|
||||
? ` codeRequest=${codeRequestEntry}(${codeRequestValue}) — ${codeRequestMessage}`
|
||||
: '';
|
||||
message = `SAT request=${reqEntry}(${reqValue})${codeReqStr} wrapperCode=${statusCode} wrapperMsg="${statusMsg}"`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: result.getStatus().isAccepted(),
|
||||
status,
|
||||
packageIds: result.getPackageIds(),
|
||||
totalCfdis: result.getNumberCfdis(),
|
||||
message,
|
||||
statusCode,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('[SAT Verify Error]', error.message || error);
|
||||
// Errores de la librería (ej. webError.getResponse is not a function)
|
||||
// no son fallos del SAT — devolver 'pending' para reintentar polling
|
||||
return {
|
||||
success: false,
|
||||
status: 'pending',
|
||||
packageIds: [],
|
||||
totalCfdis: 0,
|
||||
message: error.message || 'Error al verificar solicitud',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Descarga un paquete de CFDIs
|
||||
*/
|
||||
export async function downloadSatPackage(
|
||||
service: Service,
|
||||
packageId: string
|
||||
): Promise<DownloadResult> {
|
||||
try {
|
||||
const result = await service.download(packageId);
|
||||
|
||||
if (!result.getStatus().isAccepted()) {
|
||||
return {
|
||||
success: false,
|
||||
packageContent: '',
|
||||
message: result.getStatus().getMessage(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
packageContent: result.getPackageContent(),
|
||||
message: 'Paquete descargado',
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('[SAT Download Error]', error);
|
||||
return {
|
||||
success: false,
|
||||
packageContent: '',
|
||||
message: error.message || 'Error al descargar paquete',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea una fecha para el SAT (YYYY-MM-DD HH:mm:ss)
|
||||
*/
|
||||
function formatDateForSat(date: Date): string {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
Reference in New Issue
Block a user