import { Injectable } from '@nestjs/common';
import { spawn } from 'node:child_process';
import { DatabaseService } from '../../../infrastructure/database/database.service';
export interface ExternalValidationResult {
valid:boolean;
validator:string;
messages:Array<{code:string;severity:string;message?:string}>;
}
@Injectable()
export class EInvoiceValidatorService {
constructor(private readonly database:DatabaseService) {}
async validate(input:{
organizationId:string;
xml:string;
profile:string;
connectionId?:string;
}):Promise<ExternalValidationResult> {
if(!input.connectionId) {
return {
valid:false,
validator:'not-configured',
messages:[{
code:'external_validator_required',
severity:'error',
message:'Production e-invoice conformance validator is not configured.',
}],
};
}
const rows=await this.database.query<any>(
`select * from einvoice_validator_connections
where id=$1 and organization_id=$2 and status='active'`,
[input.connectionId,input.organizationId],
);
const c=rows[0];
if(!c) return {
valid:false,validator:'not-configured',
messages:[{code:'validator_connection_invalid',severity:'error'}],
};
if(c.provider==='mock') {
return {valid:true,validator:'mock',messages:[]};
}
if(c.provider==='http-validator') {
const url=String(c.config_json?.url??'');
if(!url) return {valid:false,validator:'http-validator',messages:[{code:'validator_url_missing',severity:'error'}]};
const response=await fetch(url,{
method:'POST',
headers:{'Content-Type':'application/xml','X-Invoice-Profile':input.profile},
body:input.xml,
signal:AbortSignal.timeout(Number(c.config_json?.timeoutMs??15000)),
});
const payload:any=await response.json().catch(()=>({}));
if(!response.ok) {
return {valid:false,validator:'http-validator',messages:[{code:`validator_http_${response.status}`,severity:'error'}]};
}
return {
valid:Boolean(payload.valid),
validator:String(payload.validator??'http-validator'),
messages:Array.isArray(payload.messages)?payload.messages:[],
};
}
if(c.provider==='command-validator') {
const command=String(c.config_json?.command??'');
const args=Array.isArray(c.config_json?.args)?c.config_json.args.map(String):[];
if(!command) return {valid:false,validator:'command-validator',messages:[{code:'validator_command_missing',severity:'error'}]};
return new Promise((resolve)=>{
const child=spawn(command,args,{stdio:['pipe','pipe','pipe']});
let stdout='',stderr='';
child.stdout.on('data',(d:Buffer)=>stdout+=d.toString());
child.stderr.on('data',(d:Buffer)=>stderr+=d.toString());
child.on('close',(code:number|null)=>{
if(code!==0) return resolve({
valid:false,validator:'command-validator',
messages:[{code:'validator_command_failed',severity:'error',message:stderr.slice(0,1000)}],
});
try {
const p=JSON.parse(stdout);
resolve({valid:Boolean(p.valid),validator:String(p.validator??'command-validator'),messages:p.messages??[]});
} catch {
resolve({valid:false,validator:'command-validator',messages:[{code:'validator_response_invalid',severity:'error'}]});
}
});
child.stdin.end(input.xml);
});
}
return {valid:false,validator:String(c.provider),messages:[{code:'validator_provider_unsupported',severity:'error'}]};
}
}
