Files
moraworld/apps/api/src/senae/senae.service.ts
T

94 lines
3.9 KiB
TypeScript

import { Injectable, Logger } from "@nestjs/common";
import { IntegrationsService } from "../integrations/integrations.service";
export interface SenaeDeclarationResult {
authNumber: string;
declarationId: string;
isStub: boolean;
message: string;
}
/**
* SenaeService — C-1
* Envía la Declaración Simplificada de Importación (DSI) a la SENAE.
* Si las credenciales de integración están configuradas, hace la llamada real.
* Si no, genera un número de stub para entorno de desarrollo.
*
* Docs: §11, §18 — SENAE WebService integration
*/
@Injectable()
export class SenaeService {
private readonly logger = new Logger(SenaeService.name);
constructor(private integrations: IntegrationsService) {}
async submitDSI(pkg: any, tenantId: string, category: string, agentNotes?: string): Promise<SenaeDeclarationResult> {
const endpoint = await this.integrations.getValue(tenantId, "senae_endpoint");
const apiKey = await this.integrations.getValue(tenantId, "senae_api_key");
const ruc = await this.integrations.getValue(tenantId, "senae_ruc");
const agentCode = await this.integrations.getValue(tenantId, "senae_agent_code");
const declarationId = `DSI-${pkg.trackingId}`;
// ── Real SENAE WebService call ────────────────────────────────────────────
if (endpoint && apiKey && ruc) {
try {
const payload = {
declaracion: {
tipo: "DSI",
rucDeclarante: ruc,
codigoAgente: agentCode ?? null,
trackingInterno: pkg.trackingId,
descripcion: pkg.description,
valorDeclarado: Number(pkg.declaredValue ?? 0),
pesoKg: Number(pkg.actualWeight ?? pkg.declaredWeight ?? 0) * 0.453592,
categoria: category,
fechaEnvio: new Date().toISOString(),
notas: agentNotes ?? null,
},
};
const res = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
"X-Agent-Code": agentCode ?? "",
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) {
const errText = await res.text().catch(() => `HTTP ${res.status}`);
this.logger.error(`[SENAE] DSI submit failed (${res.status}): ${errText}`);
// Fall through to stub on error
} else {
const data = await res.json();
const authNumber = data?.autorizacion ?? data?.authNumber ?? data?.numeroAutorizacion;
if (authNumber) {
this.logger.log(`[SENAE] DSI aprobada — Auth: ${authNumber} — Pkg: ${pkg.trackingId}`);
return { authNumber, declarationId, isStub: false, message: `DSI aprobada por SENAE. Autorización: ${authNumber}` };
}
this.logger.warn(`[SENAE] Respuesta inesperada: ${JSON.stringify(data).slice(0, 200)}`);
}
} catch (err: any) {
this.logger.error(`[SENAE] Exception: ${err.message}`);
// Fall through to stub
}
} else {
this.logger.warn(`[SENAE] Credenciales no configuradas para tenant ${tenantId} — usando stub`);
}
// ── Stub fallback (dev / sin credenciales) ────────────────────────────────
const authNumber = `SENAE-DSI-${new Date().getFullYear()}-${Math.floor(100000 + Math.random() * 900000)}`;
this.logger.warn(`[SENAE STUB] Auth: ${authNumber} — configure senae_endpoint, senae_api_key, senae_ruc en Integraciones`);
return {
authNumber,
declarationId,
isStub: true,
message: `DSI generada (STUB — sin conexión real SENAE). Auth: ${authNumber}. Configure las claves SENAE en Admin → Integraciones.`,
};
}
}