var cfdiutilsCore = require('@nodecfdi/cfdiutils-core'); var luxon = require('luxon'); var tsMixer = require('ts-mixer'); var cfdiutilsCommon = require('@nodecfdi/cfdiutils-common'); var credentials = require('@nodecfdi/credentials'); var temp = require('temp'); var xmlSchemaValidator = require('@nodecfdi/xml-schema-validator'); var fs = require('fs'); var rfc = require('@nodecfdi/rfc'); var cfdiutilsElements = require('@nodecfdi/cfdiutils-elements'); class XmlStringPropertyTrait { constructor() { this._xmlString = ''; } setXmlString(xmlString) { this._xmlString = xmlString; } getXmlString() { return this._xmlString || ''; } } exports.StatusLvl = void 0; (function (StatusLvl) { StatusLvl["ERROR"] = "ERROR"; StatusLvl["WARNING"] = "WARN"; StatusLvl["NONE"] = "NONE"; StatusLvl["OK"] = "OK"; })(exports.StatusLvl || (exports.StatusLvl = {})); class Status { constructor(value) { this._status = void 0; this.toString = () => { return this._status; }; if (value !== exports.StatusLvl.ERROR && value !== exports.StatusLvl.WARNING && value !== exports.StatusLvl.OK && value !== exports.StatusLvl.NONE) { throw new SyntaxError('The status is not one of the defined valid constants enum'); } this._status = value; } static ok() { return new Status(exports.StatusLvl.OK); } static error() { return new Status(exports.StatusLvl.ERROR); } static warn() { return new Status(exports.StatusLvl.WARNING); } static none() { return new Status(exports.StatusLvl.NONE); } isError() { return exports.StatusLvl.ERROR === this._status; } isWarning() { return exports.StatusLvl.WARNING === this._status; } isOk() { return exports.StatusLvl.OK === this._status; } isNone() { return exports.StatusLvl.NONE === this._status; } static when(condition, errorStatus = null) { return condition ? Status.ok() : errorStatus || Status.error(); } equalsTo(status) { return status._status === this._status; } compareTo(status) { return Math.sign(Status.comparableValue(this) - Status.comparableValue(status)); } static comparableValue(status) { return Object.values(exports.StatusLvl).indexOf(status._status) + 1; } } class SelloDigitalCertificadoValidatorTrait extends tsMixer.Mixin(cfdiutilsCore.XmlResolverPropertyTrait, XmlStringPropertyTrait, cfdiutilsCore.XsltBuilderPropertyTrait) { constructor(...args) { super(...args); this._asserts = void 0; this._certificado = void 0; } registerAsserts() { const asserts = { SELLO01: 'Se puede obtener el certificado del comprobante', SELLO02: 'El número de certificado del comprobante igual al encontrado en el certificado', SELLO03: 'El RFC del comprobante igual al encontrado en el certificado', SELLO04: 'El nombre del emisor del comprobante es igual al encontrado en el certificado', SELLO05: 'La fecha del documento es mayor o igual a la fecha de inicio de vigencia del certificado', SELLO06: 'La fecha del documento menor o igual a la fecha de fin de vigencia del certificado', SELLO07: 'El sello del comprobante está en base 64', SELLO08: 'El sello del comprobante coincide con el certificado y la cadena de origen generada' }; Object.entries(asserts).forEach(([code, title]) => { this._asserts.put(code, title); }); } validate(comprobante, asserts) { this._asserts = asserts; this.registerAsserts(); // create the certificate // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const extractor = new cfdiutilsCore.NodeCertificado(comprobante); let certificado; let version; try { certificado = extractor.obtain(); version = extractor.getVersion(); } catch (e) { this._asserts.putStatus('SELLO01', Status.error(), e.message); return Promise.resolve(); } this._certificado = certificado; this._asserts.putStatus('SELLO01', Status.ok()); // start validations this.validateNoCertificado(comprobante.get('NoCertificado')); const hasRegistroFiscal = comprobante.searchNodes('cfdi:Complemento', 'registrofiscal:CFDIRegistroFiscal').length > 0; const noCertificadoSAT = comprobante.searchAttribute('cfdi:Complemento', 'tfd:TimbreFiscalDigital', 'NoCertificadoSAT'); if (!hasRegistroFiscal || comprobante.get('NoCertificado') !== noCertificadoSAT) { // validate emisor rfc this.validateRfc(comprobante.searchAttribute('cfdi:Emisor', 'Rfc')); // validate emisor nombre this.validateNombre(comprobante.searchAttribute('cfdi:Emisor', 'Nombre')); } this.validateFecha(comprobante.get('Fecha')); return this.validateSello(comprobante.get('Sello'), version); } async buildCadenaOrigen(version) { const xsltLocation = await this.getXmlResolver().resolveCadenaOrigenLocation(version); return this.getXsltBuilder().build(this.getXmlString(), xsltLocation); } validateNoCertificado(noCertificado) { const expectedNumber = this._certificado.serialNumber().bytes(); this._asserts.putStatus('SELLO02', Status.when(expectedNumber === noCertificado), `Certificado: ${expectedNumber}, Comprobante: ${noCertificado}`); } validateRfc(emisorRfc) { const expectedRfc = this._certificado.rfc(); this._asserts.put('SELLO03', 'El RFC del comprobante igual al encontrado en el certificado', Status.when(expectedRfc === emisorRfc), `Rfc certificado: ${expectedRfc}, Rfc comprobante: ${emisorRfc}`); } validateNombre(emisorNombre) { if ('' === emisorNombre) { return; } this._asserts.putStatus('SELLO04', Status.when(this.compareNames(this._certificado.legalName(), emisorNombre)), `Nombre certificado: ${this._certificado.legalName()}, Nombre comprobante: ${emisorNombre}`); } validateFecha(fechaSource) { const fecha = '' === fechaSource ? 0 : luxon.DateTime.fromISO(fechaSource).toMillis(); if (0 === fecha) { return; } const validFrom = this._certificado.validFromDateTime(); const validTo = this._certificado.validToDateTime(); const explanation = `Validez del certificado: ${validFrom.toFormat('yyyy-LL-dd HH:mm:ss')} hasta ${validTo.toFormat('yyyy-LL-dd HH:mm:ss')}, Fecha comprobante ${luxon.DateTime.fromMillis(fecha).toFormat('yyyy-LL-dd HH:mm:ss')}`; this._asserts.putStatus('SELLO05', Status.when(fecha >= validFrom.toMillis()), explanation); this._asserts.putStatus('SELLO06', Status.when(fecha <= validTo.toMillis()), explanation); } async validateSello(selloBase64, version) { const sello = this.obtainSello(selloBase64); if ('' === sello) { return; } const cadena = await this.buildCadenaOrigen(version); const selloIsValid = this._certificado.publicKey().verify(cadena, Buffer.from(sello, 'binary').toString('hex')); this._asserts.putStatus('SELLO08', Status.when(selloIsValid), 'La verificación del sello del CFDI no coincide, probablemente el CFDI fue alterado o mal generado'); } obtainSello(selloBase64) { let sello; try { sello = Buffer.from(selloBase64, 'base64').toString('binary'); } catch (e) { sello = false; } this._asserts.putStatus('SELLO07', Status.when(false !== sello)); return sello || ''; } compareNames(first, second) { return this.castNombre(first) === this.castNombre(second); } castNombre(nombre) { [' ', '-', ',', '.', '#', '&', "'", '"', '~', '¨', '^'].forEach(searchString => { nombre = nombre.replace(searchString, ''); }); return nombre.toUpperCase(); } } class TimbreFiscalDigitalSelloValidatorTrait extends tsMixer.Mixin(cfdiutilsCore.XmlResolverPropertyTrait, cfdiutilsCore.XsltBuilderPropertyTrait) { async validate(comprobante, asserts) { const assert = asserts.put('TFDSELLO01', 'El sello SAT del Timbre Fiscal Digital corresponde al certificado SAT'); if (!this.hasXmlResolver()) { assert.setExplanation('No se puede hacer la validación porque carece de un objeto resolvedor'); return Promise.resolve(); } const tfd = comprobante.searchNode('cfdi:Complemento', 'tfd:TimbreFiscalDigital'); if (!tfd) { assert.setExplanation('El CFDI no contiene un Timbre Fiscal Digital'); return Promise.resolve(); } if ('1.1' !== tfd.get('Version')) { assert.setExplanation('La versión del timbre fiscal digital no es 1.1'); return Promise.resolve(); } const validationSellosMatch = comprobante.get('Sello') !== tfd.get('SelloCFD'); if (validationSellosMatch) { assert.setStatus(Status.error(), 'El atributo SelloCFD del Timbre Fiscal Digital no coincide con el atributo Sello del Comprobante'); return Promise.resolve(); } const certificadoSAT = tfd.get('NoCertificadoSAT'); if (!cfdiutilsCore.SatCertificateNumber.isValidCertificateNumber(certificadoSAT)) { assert.setStatus(Status.error(), `El atributo NoCertificadoSAT con el valor "${certificadoSAT}" no es valido`); return Promise.resolve(); } let certificado; const resolver = this.getXmlResolver(); try { const certificadoUrl = new cfdiutilsCore.SatCertificateNumber(certificadoSAT).remoteUrl(); if (!resolver.hasLocalPath()) { const temporaryFile = temp.openSync({ prefix: '.cer' }); const certificadoFile = temporaryFile.path; await resolver.getDownloader().downloadTo(certificadoUrl, certificadoFile); certificado = credentials.Certificate.openFile(certificadoFile); temp.cleanupSync(); } else { const certificadoFile = await resolver.resolve(certificadoUrl, cfdiutilsCore.XmlResolver.TYPE_CER); certificado = credentials.Certificate.openFile(certificadoFile); } } catch (e) { assert.setStatus(Status.error(), `No se ha podido obtener el certificado ${certificadoSAT}: ${e.message}`); return Promise.resolve(); } const tfdCadenaOrigen = new cfdiutilsCore.TfdCadenaDeOrigen(resolver, this.getXsltBuilder()); // fix al parecer no me regresa el namespace xmlns:xsi tfd.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); const source = await tfdCadenaOrigen.build(cfdiutilsCommon.XmlNodeUtils.nodeToXmlString(tfd), tfd.get('Version')); const signature = Buffer.from(tfd.get('SelloSAT'), 'base64').toString('hex'); const verification = certificado.publicKey().verify(source, signature); if (!verification) { assert.setStatus(Status.error(), ['La verificación del timbrado fue negativa,', ' posiblemente el CFDI fue modificado después de general el sello'].join('')); return Promise.resolve(); } assert.setStatus(Status.ok()); return Promise.resolve(); } } class TimbreFiscalDigitalVersionValidatorTrait { validate(comprobante, asserts) { asserts.put('TFDVERSION01', 'Si existe el complemento timbre fiscal digital, entonces su versión debe ser 1.1'); const tfdVersion = comprobante.searchNode('cfdi:Complemento', 'tfd:TimbreFiscalDigital'); if (tfdVersion) { asserts.putStatus('TFDVERSION01', Status.when('1.1' === tfdVersion.get('Version'))); } return Promise.resolve(); } } class XmlFollowSchema extends tsMixer.Mixin(XmlStringPropertyTrait, cfdiutilsCore.XmlResolverPropertyTrait) { canValidateCfdiVersion(_version) { return true; } async validate(comprobante, asserts) { const assert = asserts.put('XSD01', 'El contenido XML sigue los esquemas XSD'); // obtain content let content = this.getXmlString(); if ('' === content) { content = cfdiutilsCommon.XmlNodeUtils.nodeToXmlString(comprobante); } // create the schema validator object const schemaValidator = xmlSchemaValidator.SchemaValidator.createFromString(content); // validate using resolver->retriever or using the simple method try { let schemas = schemaValidator.buildSchemas(); if (this.hasXmlResolver() && this.getXmlResolver().hasLocalPath()) { schemas = await this.changeSchemasUsingRetriever(schemas); } schemaValidator.validateWithSchemas(schemas); } catch (e) { assert.setStatus(Status.error(), e.message); asserts.mustStop(true); return Promise.resolve(); } // set final status assert.setStatus(Status.ok()); return Promise.resolve(); } async changeSchemasUsingRetriever(schemas) { // obtain the retriever, throw its own exception if non set const retriever = this.getXmlResolver().newXsdRetriever(); // replace the schemas locations with the retrieved local path for (const schema of schemas.values()) { const location = schema.getLocation(); const localPath = retriever.buildPath(location); if (!fs.existsSync(localPath)) { await retriever.retrieve(location); } // this call will change the value, not insert a new entry schemas.insert(new xmlSchemaValidator.Schema(schema.getNamespace(), localPath)); } return Promise.resolve(schemas); } } class AbstractVersion33 { canValidateCfdiVersion(version) { return '3.3' === version; } } class AbstractRecepcionPagos10 extends AbstractVersion33 { validate(comprobante, asserts) { // do not run anything if not found const pagos10 = comprobante.searchNode('cfdi:Complemento', 'pago10:Pagos'); if ('3.3' !== comprobante.get('Version') || 'P' !== comprobante.get('TipoDeComprobante') || !pagos10 || '1.0' !== pagos10.get('Version')) { return Promise.resolve(); } return this.validateRecepcionPagos(comprobante, asserts); } } class AbstractDiscoverableVersion33 extends AbstractVersion33 {} class AbstractVersion40 { canValidateCfdiVersion(version) { return '4.0' === version; } } class AbstractDiscoverableVersion40 extends AbstractVersion40 {} /** * ComprobanteDecimalesMoneda * * Válida que: * - MONDEC01: El subtotal del comprobante no contiene más de los decimales de la moneda (CFDI33106) * - MONDEC02: El descuento del comprobante no contiene más de los decimales de la moneda (CFDI33111) * - MONDEC03: El total del comprobante no contiene más de los decimales de la moneda * - MONDEC04: El total de impuestos trasladados no contiene más de los decimales de la moneda (CFDI33182) * - MONDEC05: El total de impuestos retenidos no contiene más de los decimales de la moneda (CFDI33180) */ class ComprobanteDecimalesMoneda extends AbstractDiscoverableVersion33 { constructor(...args) { super(...args); this._asserts = void 0; this._currency = void 0; } registerAsserts() { const asserts = { MONDEC01: 'El subtotal del comprobante no contiene más de los decimales de la moneda (CFDI33106)', MONDEC02: 'El descuento del comprobante no contiene más de los decimales de la moneda (CFDI33111)', MONDEC03: 'El total del comprobante no contiene más de los decimales de la moneda', MONDEC04: 'El total de impuestos trasladados no contiene más de los decimales de la moneda (CFDI33182)', MONDEC05: 'El total de impuestos retenidos no contiene más de los decimales de la moneda (CFDI33180)' }; Object.entries(asserts).forEach(([code, title]) => { this._asserts.put(code, title); }); } validate(comprobante, asserts) { this._asserts = asserts; this.registerAsserts(); try { this._currency = cfdiutilsCommon.CurrencyDecimals.newFromKnownCurrencies(comprobante.get('Moneda')); } catch (e) { this._asserts.get('MONDEC01').setExplanation(e.message); return Promise.resolve(); } // SubTotal, Descuento, Total this.validateValue('MONDEC01', comprobante, 'SubTotal', true); this.validateValue('MONDEC02', comprobante, 'Descuento'); this.validateValue('MONDEC03', comprobante, 'Total', true); const impuestos = comprobante.searchNode('cfdi:Impuestos'); if (impuestos) { this.validateValue('MONDEC04', impuestos, 'TotalImpuestosTrasladados'); this.validateValue('MONDEC05', impuestos, 'TotalImpuestosRetenidos'); } return Promise.resolve(); } validateValue(code, node, attribute, required = false) { return this._asserts.putStatus(code, Status.when(this.checkValue(node, attribute, required)), `Valor: "${node.get(attribute)}", Moneda: "${this._currency.currency()}" - ${this._currency.decimals()} decimales`); } checkValue(node, attribute, required) { if (required && !node.offsetExists(attribute)) { return false; } return this._currency.doesNotExceedDecimals(node.get(attribute)); } } /** * ComprobanteDescuento * * Válida que: * - DESCUENTO01: Si existe el atributo descuento, entonces debe ser menor o igual que el subtotal * y mayor o igual que cero (CFDI33109) */ class ComprobanteDescuento extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { asserts.put('DESCUENTO01', ['Si existe el atributo descuento,', ' entonces debe ser menor o igual que el subtotal y mayor o igual que cero (CFDI33109)'].join('')); if (comprobante.offsetExists('Descuento')) { const descuento = parseFloat(comprobante.get('Descuento') || '0'); const subtotal = parseFloat(comprobante.get('SubTotal') || '0'); asserts.putStatus('DESCUENTO01', Status.when('' !== comprobante.get('Descuento') && descuento >= 0 && descuento <= subtotal), `Descuento: "${comprobante.get('Descuento')}", SubTotal: "${comprobante.get('SubTotal')}"`); } return Promise.resolve(); } } /** * ComprobanteFormaPago * * Válida que: * - FORMAPAGO01: El campo forma de pago no debe existir cuando existe el complemento para recepción de pagos * (CFDI33103) */ class ComprobanteFormaPago extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { const assert = asserts.put('FORMAPAGO01', 'El campo forma de pago no debe existir cuando existe el complemento para recepción de pagos (CFDI33103)', Status.none()); const existsComplementoPagos = undefined !== comprobante.searchNode('cfdi:Complemento', 'pago10:Pagos'); if (existsComplementoPagos) { const existsFormaPago = comprobante.offsetExists('FormaPago'); assert.setStatus(Status.when(!existsFormaPago)); } return Promise.resolve(); } } /** * ConceptoImpuestos * * Válida que: * - COMPIMPUESTOSC01: Si existe el nodo impuestos entonces debe incluir el total de traslados * y/o el total de retenciones * - COMPIMPUESTOSC02: Si existe al menos un traslado entonces debe existir el total de traslados * - COMPIMPUESTOSC03: Si existe al menos una retención entonces debe existir el total de retenciones */ class ComprobanteImpuestos extends AbstractDiscoverableVersion33 { registerAsserts(asserts) { const assertDescriptions = { COMPIMPUESTOSC01: ['Si existe el nodo impuestos entonces debe incluir el total de traslados y/o', ' el total de retenciones'].join(''), COMPIMPUESTOSC02: 'Si existe al menos un traslado entonces debe existir el total de traslados', COMPIMPUESTOSC03: 'Si existe al menos una retención entonces debe existir el total de retenciones' }; Object.entries(assertDescriptions).forEach(([code, title]) => { asserts.put(code, title); }); } validate(comprobante, asserts) { this.registerAsserts(asserts); const nodeImpuestos = comprobante.searchNode('cfdi:Impuestos'); if (!nodeImpuestos) { return Promise.resolve(); } const existsTotalTrasladados = nodeImpuestos.offsetExists('TotalImpuestosTrasladados'); const existsTotalRetenidos = nodeImpuestos.offsetExists('TotalImpuestosRetenidos'); asserts.putStatus('COMPIMPUESTOSC01', Status.when(existsTotalTrasladados || existsTotalRetenidos)); const hasTraslados = !!nodeImpuestos.searchNode('cfdi:Traslados', 'cfdi:Traslado'); asserts.putStatus('COMPIMPUESTOSC02', Status.when(!(hasTraslados && !existsTotalTrasladados))); const hasRetenciones = !!nodeImpuestos.searchNode('cfdi:Retenciones', 'cfdi:Retencion'); asserts.putStatus('COMPIMPUESTOSC03', Status.when(!(hasRetenciones && !existsTotalRetenidos))); return Promise.resolve(); } } /** * ComprobanteTipoCambio * * Válida que: * - TIPOCAMBIO01: La moneda exista y no tenga un valor vacío * - TIPOCAMBIO02: Si la moneda es "MXN", entonces el tipo de cambio debe tener el valor "1" * o no debe existir (CFDI33113) * - TIPOCAMBIO03: Si la moneda es "XXX", entonces el tipo de cambio no debe existir (CFDI33115) * - TIPOCAMBIO04: Si la moneda no es "MXN" ni "XXX", entonces el tipo de cambio entonces * el tipo de cambio debe seguir el patrón [0-9]\{1,18\}(.[0-9]\{1,6\})? (CFDI33114, CFDI33117) */ class ComprobanteTipoCambio extends AbstractDiscoverableVersion33 { registerAssets(asserts) { const assertDescriptions = { TIPOCAMBIO01: 'La moneda exista y no tenga un valor vacío', TIPOCAMBIO02: ['Si la moneda es "MXN", entonces el tipo de cambio debe tener el valor "1"', ' o no debe existir (CFDI33113)'].join(''), TIPOCAMBIO03: 'Si la moneda es "XXX", entonces el tipo de cambio no debe existir (CFDI33115)', TIPOCAMBIO04: ['Si la moneda no es "MXN" ni "XXX", entonces el tipo de cambio', ' debe seguir el patrón [0-9]{1,18}(.[0-9]{1,6}?) (CFDI33114, CFDI33117)'].join('') }; Object.entries(assertDescriptions).forEach(([code, title]) => { asserts.put(code, title); }); } validate(comprobante, asserts) { this.registerAssets(asserts); const existsTipoCambio = comprobante.offsetExists('TipoCambio'); const tipoCambio = comprobante.get('TipoCambio'); const moneda = comprobante.get('Moneda'); asserts.putStatus('TIPOCAMBIO01', Status.when('' !== moneda)); if ('' === moneda) { return Promise.resolve(); } if ('MXN' === moneda) { asserts.putStatus('TIPOCAMBIO02', Status.when(!existsTipoCambio || Math.abs(parseFloat(tipoCambio || '0') - 1) < 0.0000001)); } if ('XXX' === moneda) { asserts.putStatus('TIPOCAMBIO03', Status.when(!existsTipoCambio)); } if ('MXN' !== moneda && 'XXX' !== moneda) { const pattern = /^\d{1,18}(\.\d{1,6})?$/; asserts.putStatus('TIPOCAMBIO04', Status.when(!!tipoCambio.match(pattern))); } return Promise.resolve(); } } /** * ComprobanteTipoDeComprobante * * Válida que: * - TIPOCOMP01: Si el tipo de comprobante es T, P ó N, entonces no debe existir las condiciones de pago * - TIPOCOMP02: Si el tipo de comprobante es T, P ó N, entonces no debe existir la definición de impuestos (CFDI33179) * - TIPOCOMP03: Si el tipo de comprobante es T ó P, entonces no debe existir la forma de pago * - TIPOCOMP04: Si el tipo de comprobante es T ó P, entonces no debe existir el método de pago (CFDI33123) * - TIPOCOMP05: Si el tipo de comprobante es T ó P, entonces no debe existir el descuento del comprobante (CFDI33110) * - TIPOCOMP06: Si el tipo de comprobante es T ó P, entonces no debe existir el descuento de los conceptos (CFDI33179) * - TIPOCOMP07: Si el tipo de comprobante es T ó P, entonces el subtotal debe ser cero (CFDI33108) * - TIPOCOMP08: Si el tipo de comprobante es T ó P, entonces el total debe ser cero * - TIPOCOMP09: Si el tipo de comprobante es I, E ó N, entonces el valor unitario de todos los conceptos * debe ser mayor que cero * - TIPOCOMP010: Si el tipo de comprobante es N, entonces la moneda debe ser MXN */ class ComprobanteTipoDeComprobante extends AbstractDiscoverableVersion33 { registerAsserts(asserts) { const assertsDescriptions = { TIPOCOMP01: ['Si el tipo de comprobante es T, P ó N,', ' entonces no debe existir las condiciones de pago'].join(''), TIPOCOMP02: ['Si el tipo de comprobante es T, P ó N,', ' entonces no debe existir la definición de impuestos (CFDI33179)'].join(''), TIPOCOMP03: 'Si el tipo de comprobante es T ó P, entonces no debe existir la forma de pago', TIPOCOMP04: ['Si el tipo de comprobante es T ó P,', ' entonces no debe existir el método de pago (CFDI33123)'].join(''), TIPOCOMP05: ['Si el tipo de comprobante es T ó P,', ' entonces no debe existir el descuento del comprobante (CFDI33110)'].join(''), TIPOCOMP06: ['Si el tipo de comprobante es T ó P,', ' entonces no debe existir el descuento de los conceptos (CFDI33179)'].join(''), TIPOCOMP07: 'Si el tipo de comprobante es T ó P, entonces el subtotal debe ser cero (CFDI33108)', TIPOCOMP08: 'Si el tipo de comprobante es T ó P entonces el total debe ser cero', TIPOCOMP09: ['Si el tipo de comprobante es I, E ó N,', ' entonces el valor unitario de todos los conceptos debe ser mayor que cero'].join(''), TIPOCOMP10: 'Si el tipo de comprobante es N entonces, la moneda debe ser MXN' }; Object.entries(assertsDescriptions).forEach(([code, title]) => { asserts.put(code, title); }); } validate(comprobante, asserts) { this.registerAsserts(asserts); const tipoComprobante = comprobante.get('TipoDeComprobante'); if ('T' === tipoComprobante || 'P' === tipoComprobante || 'N' === tipoComprobante) { asserts.putStatus('TIPOCOMP01', Status.when(!comprobante.offsetExists('CondicionesDePago'))); asserts.putStatus('TIPOCOMP02', Status.when(!comprobante.searchNode('cfdi:Impuestos'))); } if ('T' === tipoComprobante || 'P' === tipoComprobante) { asserts.putStatus('TIPOCOMP03', Status.when(!comprobante.offsetExists('FormaPago'))); asserts.putStatus('TIPOCOMP04', Status.when(!comprobante.offsetExists('MetodoPago'))); asserts.putStatus('TIPOCOMP05', Status.when(!comprobante.offsetExists('Descuento'))); asserts.putStatus('TIPOCOMP06', Status.when(this.checkConceptosDoesNotHaveDescuento(comprobante))); asserts.putStatus('TIPOCOMP07', Status.when(this.isZero(comprobante.get('SubTotal')))); asserts.putStatus('TIPOCOMP08', Status.when(this.isZero(comprobante.get('Total')))); } if ('I' === tipoComprobante || 'E' === tipoComprobante || 'N' === tipoComprobante) { asserts.putStatus('TIPOCOMP09', Status.when(this.checkConceptosValorUnitarioIsGreaterThanZero(comprobante))); } if ('N' === tipoComprobante) { asserts.putStatus('TIPOCOMP10', Status.when('MXN' === comprobante.get('Moneda'))); } return Promise.resolve(); } checkConceptosDoesNotHaveDescuento(comprobante) { for (const concepto of comprobante.searchNodes('cfdi:Conceptos', 'cfdi:Concepto')) { if (concepto.offsetExists('Descuento')) { return false; } } return true; } checkConceptosValorUnitarioIsGreaterThanZero(comprobante) { for (const concepto of comprobante.searchNodes('cfdi:Conceptos', 'cfdi:Concepto')) { if (!this.isGreaterThanZero(concepto.get('ValorUnitario'))) { return false; } } return true; } isZero(value = '') { if ('' === value || isNaN(Number(value))) { return false; } return Math.abs(parseFloat(value)) < 0.0000001; } isGreaterThanZero(value = '') { if ('' === value || isNaN(Number(value))) { return false; } return Math.abs(parseFloat(value)) > 0.0000001; } } /** * ComprobanteTotal * * Válida que: * - TOTAL01: El atributo Total existe, no está vacío y cumple con el patrón [0-9]+(.[0-9]+)? */ class ComprobanteTotal extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { const pattern = /^\d+(\.\d+)?$/; asserts.put('TOTAL01', 'El atributo Total existe, no está vacío y cumple con el patrón [0-9]+(.[0-9]+)?', Status.when('' !== comprobante.get('Total') && !!comprobante.get('Total').match(pattern))); return Promise.resolve(); } } /** * ConceptoDescuento * * Válida que: * - CONCEPDESC01: Si existe el atributo descuento en el concepto, * entonces debe ser menor o igual que el importe y mayor o igual que cero (CFDI33151) */ class ConceptoDescuento extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { asserts.put('CONCEPDESC01', ['Si existe el atributo descuento en el concepto,', ' entonces debe ser menor o igual que el importe y mayor o igual que cero (CFDI33151)'].join('')); let checked = 0; comprobante.searchNodes('cfdi:Conceptos', 'cfdi:Concepto').forEach((concepto, i) => { checked = checked + 1; if (this.conceptoHasInvalidDiscount(concepto)) { const explanation = `Concepto #${i}, Descuento: "${concepto.get('Descuento')}", Importe: "${concepto.get('Importe')}"`; asserts.putStatus('CONCEPDESC01', Status.error(), explanation); } }); if (checked > 0 && asserts.get('CONCEPDESC01').getStatus().isNone()) { asserts.putStatus('CONCEPDESC01', Status.ok(), `Revisados ${checked} conceptos`); } return Promise.resolve(); } conceptoHasInvalidDiscount(concepto) { if (!concepto.offsetExists('Descuento')) { return false; } const descuento = parseFloat(concepto.get('Descuento') || '0'); const importe = parseFloat(concepto.get('Importe') || '0'); return !(descuento >= 0 && descuento <= importe); } } /** * ConceptoImpuestos * * Válida que: * - CONCEPIMPC01: El nodo impuestos de un concepto debe incluir traslados y/o retenciones (CFDI33152) * - CONCEPIMPC02: Los traslados de los impuestos de un concepto deben tener una base y ser mayor a cero (CFDI33154) * - CONCEPIMPC03: No se debe registrar la tasa o cuota ni el importe cuando * el tipo de factor de traslado es exento (CFDI33157) * - CONCEPIMPC04: Se debe registrar la tasa o cuota y el importe cuando * el tipo de factor de traslado es tasa o cuota (CFDI33158) * - CONCEPIMPC05: Las retenciones de los impuestos de un concepto deben tener una base y ser mayor a cero (CFDI33154) * - CONCEPIMPC06: Las retenciones de los impuestos de un concepto deben tener * un tipo de factor diferente de exento (CFDI33166) */ class ConceptoImpuestos extends AbstractDiscoverableVersion33 { registerAsserts(asserts) { const assertsDescriptions = { CONCEPIMPC01: 'El nodo impuestos de un concepto debe incluir traslados y/o retenciones (CFDI33152)', CONCEPIMPC02: ['Los traslados de los impuestos de un concepto deben tener una base y ser mayor a cero', ' (CFDI33154)'].join(''), CONCEPIMPC03: ['No se debe registrar la tasa o cuota ni el importe cuando el tipo de factor de traslado', ' es exento (CFDI33157)'].join(''), CONCEPIMPC04: ['Se debe registrar la tasa o cuota y el importe cuando el tipo de factor de traslado', ' es tasa o cuota (CFDI33158)'].join(''), CONCEPIMPC05: ['Las retenciones de los impuestos de un concepto deben tener una base y ser mayor a cero', '(CFDI33154)'].join(''), CONCEPIMPC06: ['Las retenciones de los impuestos de un concepto deben tener un tipo de factor diferente', ' de exento (CFDI33166)'].join('') }; Object.entries(assertsDescriptions).forEach(([code, title]) => { asserts.put(code, title); }); } validate(comprobante, asserts) { this.registerAsserts(asserts); let status01 = Status.ok(); let status02 = Status.ok(); let status03 = Status.ok(); let status04 = Status.ok(); let status05 = Status.ok(); let status06 = Status.ok(); comprobante.searchNodes('cfdi:Conceptos', 'cfdi:Concepto').forEach((concepto, i) => { if (status01.isOk() && !this.conceptoImpuestosHasTrasladosOrRetenciones(concepto)) { status01 = Status.error(); asserts.get('CONCEPIMPC01').setExplanation(`Concepto #${i}`); } const traslados = concepto.searchNodes('cfdi:Impuestos', 'cfdi:Traslados', 'cfdi:Traslado'); traslados.forEach((traslado, k) => { if (status02.isOk() && !this.impuestoHasBaseGreaterThanZero(traslado)) { status02 = Status.error(); asserts.get('CONCEPIMPC02').setExplanation(`Concepto #${i}, Traslado #${k}`); } if (status03.isOk() && !this.trasladoHasTipoFactorExento(traslado)) { status03 = Status.error(); asserts.get('CONCEPIMPC03').setExplanation(`Concepto #${i}, Traslado #${k}`); } if (status04.isOk() && !this.trasladoHasTipoFactorTasaOCuota(traslado)) { status04 = Status.error(); asserts.get('CONCEPIMPC04').setExplanation(`Concepto #${i}, Traslado #${k}`); } }); const retenciones = concepto.searchNodes('cfdi:Impuestos', 'cfdi:Retenciones', 'cfdi:Retencion'); retenciones.forEach((retencion, j) => { if (status05.isOk() && !this.impuestoHasBaseGreaterThanZero(retencion)) { status05 = Status.error(); asserts.get('CONCEPIMPC05').setExplanation(`Concepto #${i}, Retención #${j}`); } if (status06.isOk() && 'Exento' === retencion.attributes().get('TipoFactor')) { status06 = Status.error(); asserts.get('CONCEPIMPC06').setExplanation(`Concepto #${i}, Retención #${j}`); } }); }); asserts.putStatus('CONCEPIMPC01', status01); asserts.putStatus('CONCEPIMPC02', status02); asserts.putStatus('CONCEPIMPC03', status03); asserts.putStatus('CONCEPIMPC04', status04); asserts.putStatus('CONCEPIMPC05', status05); asserts.putStatus('CONCEPIMPC06', status06); return Promise.resolve(); } conceptoImpuestosHasTrasladosOrRetenciones(concepto) { const impuestos = concepto.searchNode('cfdi:Impuestos'); if (!impuestos) { return true; } return impuestos.searchNodes('cfdi:Traslados', 'cfdi:Traslado').length !== 0 || impuestos.searchNodes('cfdi:Retenciones', 'cfdi:Retencion').length !== 0; } impuestoHasBaseGreaterThanZero(impuesto) { if (!impuesto.offsetExists('Base')) { return false; } if (isNaN(Number(impuesto.get('Base')))) { return false; } return parseFloat(impuesto.get('Base')) >= 0.000001; } trasladoHasTipoFactorExento(traslado) { if ('Exento' === traslado.get('TipoFactor')) { if (traslado.offsetExists('TasaOCuota')) { return false; } if (traslado.offsetExists('Importe')) { return false; } } return true; } trasladoHasTipoFactorTasaOCuota(traslado) { if ('Tasa' === traslado.get('TipoFactor') || 'Cuota' === traslado.get('TipoFactor')) { if ('' === traslado.get('TasaOCuota')) { return false; } if ('' === traslado.get('Importe')) { return false; } } return true; } } /** * EmisorRegimenFiscal * * Válida que: * - REGFIS01: El régimen fiscal contenga un valor apropiado según el tipo de RFC emisor (CFDI33130 y CFDI33131) * * Nota: No válida que el RFC sea válido, esa responsabilidad no es de este validador. */ class EmisorRegimenFiscal extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { const regimenFiscal = comprobante.searchAttribute('cfdi:Emisor', 'RegimenFiscal'); const emisorRfc = comprobante.searchAttribute('cfdi:Emisor', 'Rfc'); const length = emisorRfc.length; let validCodes; if (12 === length) { validCodes = ['601', '603', '609', '610', '620', '622', '623', '624', '626', '628']; } else if (13 === length) { validCodes = ['605', '606', '607', '608', '610', '611', '612', '614', '615', '616', '621', '625', '626', '629', '630']; } else { validCodes = []; } asserts.put('REGFIS01', 'El régimen fiscal contenga un valor apropiado según el tipo de RFC emisor (CFDI33130 y CFDI33131)', Status.when(validCodes.includes(regimenFiscal.trim())), `Rfc: "${emisorRfc}", Regimen Fiscal: "${regimenFiscal}"`); return Promise.resolve(); } } /** * EmisorRfc * * Válida que: * - EMISORRFC01: El RFC del emisor del comprobante debe ser válido y diferente de XAXX010101000 y XEXX010101000 */ class EmisorRfc extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { const assert = asserts.put('EMISORRFC01', 'El RFC del emisor del comprobante debe ser válido y diferente de XAXX010101000 y XEXX010101000'); const emisorRfc = comprobante.searchAttribute('cfdi:Emisor', 'Rfc'); try { rfc.Rfc.checkIsValid(emisorRfc, rfc.Rfc.DISALLOW_FOREIGN | rfc.Rfc.DISALLOW_GENERIC); } catch (e) { assert.setStatus(Status.error(), `Rfc: "${emisorRfc}". ${e.message}`); return Promise.resolve(); } assert.setStatus(Status.ok()); return Promise.resolve(); } } class AssertFechaFormat { static assertFormat(asserts, code, label, text) { const hasFormat = AssertFechaFormat.hasFormat(text); asserts.put(code, `La fecha ${label} cumple con el formato`, Status.when(hasFormat), `Contenido del campo: "${text}"`); return hasFormat; } static hasFormat(format) { if ('' === format) { return false; } try { const rawDate = luxon.DateTime.fromISO(format); const value = rawDate.toMillis(); const expectedFormat = luxon.DateTime.fromMillis(value).toFormat("yyyy-LL-dd'T'HH:mm:ss"); return expectedFormat === format; } catch (e) { return false; } } } /** * FechaComprobante * * Válida que: * - FECHA01: La fecha del comprobante cumple con el formato * - FECHA02: La fecha existe en el comprobante y es mayor que 2017-07-01 y menor que el futuro * - La fecha en el futuro se puede configurar a un valor determinado * - La fecha en el futuro es por defecto el momento de validación más una tolerancia * - La tolerancia puede ser configurada y es por defecto 300 segundos */ class FechaComprobante extends AbstractDiscoverableVersion33 { constructor(...args) { super(...args); this._maximumDate = void 0; this._tolerance = 300; } getMinimumDate() { return luxon.DateTime.fromObject({ hour: 0, minute: 0, second: 0, month: 7, day: 1, year: 2017 }).toMillis(); } getMaximumDate() { if (this._maximumDate === undefined || isNaN(Number(this._maximumDate))) { return Date.now() + this.getTolerance(); } return this._maximumDate; } setMaximumDate(maximumDate = null) { this._maximumDate = maximumDate !== null ? maximumDate : undefined; } getTolerance() { return this._tolerance; } setTolerance(tolerance) { this._tolerance = tolerance; } validate(comprobante, asserts) { const fechaSource = comprobante.get('Fecha'); const hasFormat = AssertFechaFormat.assertFormat(asserts, 'FECHA01', 'del comprobante', fechaSource); const assertBetween = asserts.put('FECHA02', 'La fecha existe en el comprobante y es mayor que 2017-07-01 y menor que el futuro'); if (!hasFormat) { return Promise.resolve(); } const exists = comprobante.offsetExists('Fecha'); const testDate = '' !== fechaSource ? luxon.DateTime.fromISO(fechaSource).toMillis() : 0; const minimumDate = this.getMinimumDate(); const maximumDate = this.getMaximumDate(); assertBetween.setStatus(Status.when(testDate >= minimumDate && testDate <= maximumDate), `Fecha: "${fechaSource}" (${exists ? 'Existe' : 'No existe'}), Máxima: ${luxon.DateTime.fromMillis(maximumDate).toFormat('yyyy-LL-dd HH:mm:ss')}`); return Promise.resolve(); } } /** * ReceptorResidenciaFiscal * * Válida que: * - RESFISC01: Si el RFC no es XEXX010101000, entonces la residencia fiscal no debe existir (CFDI33134) * - RESFISC02: Si el RFC sí es XEXX010101000 y existe el complemento de comercio exterior, * entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136) * - RESFISC03: Si el RFC sí es XEXX010101000 y se registró el número de registro de identificación fiscal, * entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136) */ class ReceptorResidenciaFiscal extends AbstractDiscoverableVersion33 { registerAsserts(asserts) { const assertsDescriptions = { RESFISC01: 'Si el RFC no es XEXX010101000, entonces la residencia fiscal no debe existir (CFDI33134)', RESFISC02: ['Si el RFC sí es XEXX010101000 y existe el complemento de comercio exterior,', ' entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136)'].join(''), RESFISC03: ['Si el RFC sí es XEXX010101000 y se registró el número de registro de identificación fiscal,', ' entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136)'].join('') }; Object.entries(assertsDescriptions).forEach(([code, title]) => { asserts.put(code, title); }); } validate(comprobante, asserts) { this.registerAsserts(asserts); let receptor = comprobante.searchNode('cfdi:Receptor'); if (!receptor) { receptor = new cfdiutilsCommon.CNode('cfdi:Receptor'); } if ('XEXX010101000' !== receptor.get('Rfc')) { asserts.putStatus('RESFISC01', Status.when(!receptor.offsetExists('ResidenciaFiscal'))); } const existsComercioExterior = comprobante.searchNode('cfdi:Complemento', 'cce11:ComercioExterior') !== undefined; const isValidResidenciaFiscal = '' !== receptor.get('ResidenciaFiscal') && 'MEX' !== receptor.get('ResidenciaFiscal'); if (existsComercioExterior) { asserts.putStatus('RESFISC02', Status.when(isValidResidenciaFiscal)); } if (receptor.offsetExists('NumRegIdTrib')) { asserts.putStatus('RESFISC03', Status.when(isValidResidenciaFiscal)); } return Promise.resolve(); } } /** * ReceptorRfc * * Válida que: * - RECRFC01: El RFC del receptor del comprobante debe ser válido */ class ReceptorRfc extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { const assert = asserts.put('RECRFC01', 'El RFC del receptor del comprobante debe ser válido'); const receptorRfc = comprobante.searchAttribute('cfdi:Receptor', 'Rfc'); try { rfc.Rfc.checkIsValid(receptorRfc); } catch (e) { assert.setStatus(Status.error(), `Rfc: "${receptorRfc}". ${e.message}`); return Promise.resolve(); } assert.setStatus(Status.ok()); return Promise.resolve(); } } /** * SelloDigitalCertificado * * Válida que: * - SELLO01: Se puede obtener el certificado del comprobante * - SELLO02: El número de certificado del comprobante igual al encontrado en el certificado * - SELLO03: El RFC del comprobante igual al encontrado en el certificado * - SELLO04: El nombre del emisor del comprobante es igual al encontrado en el certificado * - SELLO05: La fecha del documento es mayor o igual a la fecha de inicio de vigencia del certificado * - SELLO06: La fecha del documento menor o igual a la fecha de fin de vigencia del certificado * - SELLO07: El sello del comprobante está en base 64 * - SELLO08: El sello del comprobante coincide con el certificado y la cadena de origen generada */ class SelloDigitalCertificado$1 extends tsMixer.Mixin(AbstractDiscoverableVersion33, SelloDigitalCertificadoValidatorTrait) {} /** * TimbreFiscalDigitalSello * * Válida que: * - TFDSELLO01: El Sello SAT del Timbre Fiscal Digital corresponde al certificado SAT */ class TimbreFiscalDigitalSello$1 extends tsMixer.Mixin(AbstractDiscoverableVersion33, TimbreFiscalDigitalSelloValidatorTrait) {} /** * TimbreFiscalDigitalVersion * * Válida que: * - TFDVERSION01: Si existe el complemento timbre fiscal digital, entonces su versión debe ser 1.1 */ class TimbreFiscalDigitalVersion$1 extends tsMixer.Mixin(AbstractDiscoverableVersion33, TimbreFiscalDigitalVersionValidatorTrait) {} /** * CfdiRelacionados * * - PAGREL01: El tipo de relación en los CFDI relacionados debe ser "04" */ class CfdiRelacionados extends AbstractRecepcionPagos10 { validateRecepcionPagos(comprobante, asserts) { const assert = asserts.put('PAGREL01', 'El tipo de relación en los CFDI relacionados debe ser "04"'); const cfdiRelacionados = comprobante.searchNode('cfdi:CfdiRelacionados'); if (!cfdiRelacionados) { return Promise.resolve(); } assert.setStatus(Status.when('04' === cfdiRelacionados.get('TipoRelacion')), `Tipo de relación: "${cfdiRelacionados.get('TipoRelacion')}"`); return Promise.resolve(); } } /** * ComplementoPagos * * Este complemento se ejecuta siempre * * - COMPPAG01: El complemento de pagos debe existir si el tipo de comprobante es P y viceversa * - COMPPAG02: Si el complemento de pagos existe su version debe ser 1.0 * - COMPPAG03: Si el tipo de comprobante es P su versión debe ser 3.3 * - COMPPAG04: No debe existir el nodo impuestos del complemento de pagos (CRP237) */ class ComplementoPagos extends AbstractDiscoverableVersion33 { validate(comprobante, asserts) { asserts.put('COMPPAG01', 'El complemento de pagos debe existir si el tipo de comprobante es P y viceversa'); asserts.put('COMPPAG02', 'Si el complemento de pagos existe su version debe ser 1.0'); asserts.put('COMPPAG03', 'Si el tipo de comprobante es P su versión debe ser 3.3'); asserts.put('COMPPAG04', 'No debe existir el nodo impuestos del complemento de pagos (CRP237)'); let pagosExists = true; let pagos10 = comprobante.searchNode('cfdi:Complemento', 'pago10:Pagos'); if (!pagos10) { pagosExists = false; pagos10 = new cfdiutilsCommon.CNode('pago10:Pagos'); // avoid accessing a null object } const isTipoPago = 'P' === comprobante.get('TipoDeComprobante'); asserts.putStatus('COMPPAG01', Status.when(!(isTipoPago ? !pagosExists : pagosExists)), `Tipo de comprobante: "${comprobante.get('TipoDeComprobante')}", Complemento: "${pagosExists ? 'existe' : 'no existe'}"`); if (pagosExists) { asserts.putStatus('COMPPAG02', Status.when('1.0' === pagos10.get('Version'))); } if (isTipoPago) { asserts.putStatus('COMPPAG03', Status.when('3.3' === comprobante.get('Version'))); } asserts.putStatus('COMPPAG04', Status.when(!pagos10.searchNode('pago10:Impuestos'))); return Promise.resolve(); } } /** * ComprobantePagos - Válida los datos relacionados con el nodo Comprobante cuando es un CFDI de recepción de pagos * * - PAGCOMP01: Debe existir un solo nodo que represente el complemento de pagos * - PAGCOMP02: La forma de pago no debe existir (CRP104) * - PAGCOMP03: Las condiciones de pago no deben existir (CRP106) * - PAGCOMP04: El método de pago no deben existir (CRP105) * - PAGCOMP05: La moneda debe ser "XXX" (CRP103) * - PAGCOMP06: El tipo de cambio no debe existir (CRP108) * - PAGCOMP07: El descuento no debe existir (CRP107) * - PAGCOMP08: El subtotal del documento debe ser cero "0" (CRP102) * - PAGCOMP09: El total del documento debe ser cero "0" (CRP109) * - PAGCOMP10: No se debe registrar el apartado de Impuestos en el CFDI (CRP122) */ class ComprobantePagos extends AbstractRecepcionPagos10 { validateRecepcionPagos(comprobante, asserts) { const pagos = comprobante.searchNodes('cfdi:Complemento', 'pago10:Pagos'); asserts.put('PAGCOMP01', 'Debe existir un solo nodo que represente el complemento de pagos', Status.when(1 === pagos.length), `Encontrados: ${pagos.length}`); asserts.put('PAGCOMP02', 'La forma de pago no debe existir (CRP104)', Status.when(!comprobante.offsetExists('FormaPago'))); asserts.put('PAGCOMP03', 'Las condiciones de pago no deben existir (CRP106)', Status.when(!comprobante.offsetExists('CondicionesDePago'))); asserts.put('PAGCOMP04', 'El método de pago no debe existir (CRP105)', Status.when(!comprobante.offsetExists('MetodoPago'))); asserts.put('PAGCOMP05', 'La moneda debe ser "XXX" (CRP103)', Status.when('XXX' === comprobante.get('Moneda')), `Moneda: "${comprobante.get('Moneda')}"`); asserts.put('PAGCOMP06', 'El tipo de cambio no debe existir (CRP108)', Status.when(!comprobante.offsetExists('TipoCambio'))); asserts.put('PAGCOMP07', 'El descuento no debe existir (CRP107)', Status.when(!comprobante.offsetExists('Descuento'))); asserts.put('PAGCOMP08', 'El subtotal del documento debe ser cero "0" (CRP102)', Status.when('0' === comprobante.get('SubTotal')), `SubTotal: "${comprobante.get('SubTotal')}"`); asserts.put('PAGCOMP09', 'El total del documento debe ser cero "0" (CRP109)', Status.when('0' === comprobante.get('Total')), `Total: "${comprobante.get('Total')}"`); asserts.put('PAGCOMP10', 'No se debe registrar el apartado de Impuesto en el CFDI (CRP122)', Status.when(0 === comprobante.searchNodes('cfdi:Impuestos').length)); return Promise.resolve(); } } /** * Conceptos * En un CFDI de recepción de pagos el Concepto del CFDI debe tener datos fijos, * puede ver el problema específico en la explicación del issue * * - PAGCON01: Se debe usar el concepto predefinido (CRP107 - CRP121) */ class Conceptos extends AbstractRecepcionPagos10 { validateRecepcionPagos(comprobante, asserts) { const assert = asserts.put('PAGCON01', 'Se debe usar el concepto predefinido (CRP107 - CRP121)'); // get conceptos try { this.checkConceptos(comprobante); } catch (e) { assert.setStatus(Status.error(), e.message); return Promise.resolve(); } assert.setStatus(Status.ok()); return Promise.resolve(); } checkConceptos(comprobante) { const conceptos = comprobante.searchNode('cfdi:Conceptos'); if (!conceptos) { throw new Error('No se encontró el nodo Conceptos'); } // check conceptos count const conceptosCount = conceptos.children().length; if (1 !== conceptosCount) { throw new Error(`Se esperaba encontrar un solo hijo de conceptos, se encontraron ${conceptosCount}`); } // check it contains a Concepto const concepto = conceptos.searchNode('cfdi:Concepto'); if (!concepto) { throw new Error('No se encontró el nodo Concepto'); } // check concepto does not have any children const conceptoCount = concepto.children().length; if (0 !== conceptoCount) { throw new Error(`Se esperaba encontrar ningún hijo de concepto, se encontraron ${conceptoCount}`); } if (Conceptos.REQUIRED_CLAVEPRODSERV !== concepto.get('ClaveProdServ')) { throw new Error(`La clave del producto o servicio debe ser "${Conceptos.REQUIRED_CLAVEPRODSERV}" y se registró "${concepto.get('ClaveProdServ')}"`); } if (concepto.offsetExists('NoIdentificacion')) { throw new Error('No debe existir el número de identificación'); } if (!Conceptos.REQUIRED_CANTIDAD.test(concepto.get('Cantidad'))) { throw new Error(`La cantidad debe ser "${Conceptos.REQUIRED_CANTIDAD}" y se registró ${concepto.get('Cantidad')}`); } if (Conceptos.REQUIRED_CLAVEUNIDAD !== concepto.get('ClaveUnidad')) { throw new Error(`La clave de unidad debe ser "${Conceptos.REQUIRED_CLAVEUNIDAD}" y se registró ${concepto.get('ClaveUnidad')}`); } if (concepto.offsetExists('Unidad')) { throw new Error('No debe existir la unidad'); } if (Conceptos.REQUIRED_DESCRIPCION !== concepto.get('Descripcion')) { throw new Error(`La descripción debe ser "${Conceptos.REQUIRED_DESCRIPCION}" y se registró "${concepto.get('Descripcion')}"`); } if (!Conceptos.REQUIRED_VALORUNITARIO.test(concepto.get('ValorUnitario'))) { throw new Error(`El valor unitario debe ser "${Conceptos.REQUIRED_VALORUNITARIO}" y se registró "${concepto.get('ValorUnitario')}"`); } if (!Conceptos.REQUIRED_IMPORTE.test(concepto.get('Importe'))) { throw new Error(`El importe debe ser "${Conceptos.REQUIRED_IMPORTE}" y se registró "${concepto.get('Importe')}"`); } if (concepto.offsetExists('Descuento')) { throw new Error('No debe existir descuento'); } } } Conceptos.REQUIRED_CLAVEPRODSERV = '84111506'; Conceptos.REQUIRED_CANTIDAD = /^1(\.0{1,6})?$/; Conceptos.REQUIRED_CLAVEUNIDAD = 'ACT'; Conceptos.REQUIRED_DESCRIPCION = 'Pago'; Conceptos.REQUIRED_VALORUNITARIO = /^0(\.0{1,6})?$/; Conceptos.REQUIRED_IMPORTE = /^0(\.0{1,6})?$/; class Assert { /** * Assert constructor * * @param code - * @param title - * @param status - If null the status will be NONE * @param explanation - */ constructor(code, title = '', status = null, explanation = '') { this._title = void 0; this._status = void 0; this._explanation = void 0; this._code = void 0; this.toString = () => { return `${this._status}: ${this._code} - ${this._title}`; }; if ('' == code) { throw new SyntaxError('Code cannot be an empty string'); } this._code = code; this._title = title; this.setStatus(status || Status.none()); this._explanation = explanation; } getTitle() { return this._title; } getStatus() { return this._status; } getExplanation() { return this._explanation; } getCode() { return this._code; } setTitle(title) { this._title = title; } setStatus(status, explanation) { this._status = status; if (explanation) { this.setExplanation(explanation); } } setExplanation(explanation) { this._explanation = explanation; } } let _Symbol$iterator$1; _Symbol$iterator$1 = Symbol.iterator; class Asserts { constructor() { this._assets = new Map(); this._mustStop = false; } get length() { return this._assets.size; } /** * This will try to create a new assert or get and change assert with the same code * The new values are preserved, except if they are null * * @param code - * @param title - * @param status - * @param explanation - */ put(code, title = null, status = null, explanation = null) { let assert; if (!this.exists(code)) { assert = new Assert(code, title || '', status, explanation || ''); this.add(assert); return assert; } assert = this.get(code); if (title) { assert.setTitle(title); } if (status) { assert.setStatus(status); } if (explanation) { assert.setExplanation(explanation); } return assert; } /** * This will try to create a new assert or get and change assert with the same code * The new values are preserved, except if they are null * * @param code - * @param status - * @param explanation - */ putStatus(code, status = null, explanation = null) { return this.put(code, null, status, explanation); } /** * Get and or set the flag that alerts about stop flow * Consider this flag as: "Something was found, you should not continue" * * @param newValue - Value of the flag, if null then will not change the flag * @returns the previous value of the flag */ mustStop(newValue = null) { if (null == newValue) { return this._mustStop; } const previous = this._mustStop; this._mustStop = newValue; return previous; } hasStatus(status) { return undefined !== this.getFirstStatus(status); } hasErrors() { return this.hasStatus(Status.error()); } hasWarnings() { return this.hasStatus(Status.warn()); } getFirstStatus(status) { for (const [, assert] of this) { if (status.equalsTo(assert.getStatus())) { return assert; } } return undefined; } byStatus(status) { return new Map([...this].filter(([, item]) => status.equalsTo(item.getStatus()))); } get(code) { for (const [, assert] of this) { if (assert.getCode() === code) { return assert; } } throw new Error(`There is no assert with code ${code}`); } exists(code) { return this._assets.has(code); } oks() { return this.byStatus(Status.ok()); } errors() { return this.byStatus(Status.error()); } warnings() { return this.byStatus(Status.warn()); } nones() { return this.byStatus(Status.none()); } add(assert) { this._assets.set(assert.getCode(), assert); } indexOf(assert) { const index = [...this.values()].indexOf(assert); let indexKey = ''; if (index !== -1) { indexKey = [...this.keys()][index]; } return indexKey; } remove(assert) { const index = this.indexOf(assert); if (index !== '') { this._assets.delete(index); } } removeByCode(code) { this._assets.delete(code); } removeAll() { this._assets.clear(); } import(asserts) { for (const [, assert] of asserts) { this.add(Object.assign(Object.create(Object.getPrototypeOf(assert)), assert)); } this.mustStop(asserts.mustStop()); } // Iterators of asserts [_Symbol$iterator$1]() { return this._assets[Symbol.iterator](); } entries() { return this._assets.entries(); } keys() { return this._assets.keys(); } values() { return this._assets.values(); } } class FormaPagoEntry { constructor(entry) { this._key = void 0; this._description = void 0; this._allowSenderRfc = void 0; this._allowSenderAccount = void 0; this._senderAccountPattern = void 0; this._allowReceiverRfc = void 0; this._allowReceiverAccount = void 0; this._receiverAccountPattern = void 0; this._allowPaymentSignature = void 0; if ('' === entry.key) { throw new Error('The FormaPago key cannot be empty'); } if ('' === entry.description) { throw new Error('The FormaPago description cannot be empty'); } this._key = entry.key; this._description = entry.description; this._allowSenderRfc = entry.useSenderRfc; this._allowSenderAccount = entry.useSenderAccount; this._senderAccountPattern = this.pattern(entry.useSenderAccount, entry.useSenderAccountRegExp); this._allowReceiverRfc = entry.useReceiverRfc; this._allowReceiverAccount = entry.useReceiverAccount; this._receiverAccountPattern = this.pattern(entry.useReceiverAccount, entry.useReceiverAccountRegExp); this._allowPaymentSignature = entry.allowPaymentSignature; } pattern(allowed, pattern) { if (!allowed || !pattern) { return /^$/; } return pattern; } key() { return this._key; } description() { return this._description; } allowSenderRfc() { return this._allowSenderRfc; } allowSenderAccount() { return this._allowSenderAccount; } senderAccountPattern() { return this._senderAccountPattern; } allowReceiverRfc() { return this._allowReceiverRfc; } allowReceiverAccount() { return this._allowReceiverAccount; } receiverAccountPattern() { return this._receiverAccountPattern; } allowPaymentSignature() { return this._allowPaymentSignature; } } class FormaPagoCatalog { obtain(key) { const map = [{ key: '01', description: 'Efectivo', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '02', description: 'Cheque nominativo', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^(\d{11}|\d{18})$/, useReceiverRfc: true, useReceiverAccount: true, useReceiverAccountRegExp: /^(\d{10,11}|\d{15,16}|\d{18}|[A-Z0-9_]{10,50})$/, allowPaymentSignature: false }, { key: '03', description: 'Transferencia electrónica de fondos', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^(\d{10}|\d{16}|\d{18})$/, useReceiverRfc: true, useReceiverAccount: true, useReceiverAccountRegExp: /^(\d{10}|\d{18})$/, allowPaymentSignature: true }, { key: '04', description: 'Tarjeta de crédito', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^(\d{16})$/, useReceiverRfc: true, useReceiverAccount: true, useReceiverAccountRegExp: /^(\d{10,11}|\d{15,16}|\d{18}|[A-Z0-9_]{10,50})$/, allowPaymentSignature: false }, { key: '05', description: 'Monedero electrónico', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^(\d{10,11}|\d{15,16}|\d{18}|[A-Z0-9_]{10,50})$/, useReceiverRfc: true, useReceiverAccount: true, useReceiverAccountRegExp: /^(\d{10,11}|\d{15,16}|\d{18}|[A-Z0-9_]{10,50})$/, allowPaymentSignature: false }, { key: '06', description: 'Dinero electrónico', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^(\d{10})$/, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '08', description: 'Vales de despensa', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '12', description: 'Dación en pago', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '13', description: 'Pago por subrogación', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '14', description: 'Pago por consignación', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '15', description: 'Condonación', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '17', description: 'Compensación', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '23', description: 'Novación', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '24', description: 'Confusión', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '25', description: 'Remisión de deuda', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '26', description: 'Prescripción o caducidad', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '27', description: 'A satisfacción del acreedor', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '28', description: 'Tarjeta de débito', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^(\d{16})$/, useReceiverRfc: true, useReceiverAccount: true, useReceiverAccountRegExp: /^(\d{10,11}|\d{15,16}|\d{18}|[A-Z0-9_]{10,50})$/, allowPaymentSignature: false }, { key: '29', description: 'Tarjeta de servicios', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^(\d{15,16})$/, useReceiverRfc: true, useReceiverAccount: true, useReceiverAccountRegExp: /^(\d{10,11}|\d{15,16}|\d{18}|[A-Z0-9_]{10,50})$/, allowPaymentSignature: false }, { key: '30', description: 'Aplicación de anticipos', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '31', description: 'Intermediario pagos', useSenderRfc: false, useSenderAccount: false, useSenderAccountRegExp: undefined, useReceiverRfc: false, useReceiverAccount: false, useReceiverAccountRegExp: undefined, allowPaymentSignature: false }, { key: '99', description: 'Por definir', useSenderRfc: true, useSenderAccount: true, useSenderAccountRegExp: /^S*$/, useReceiverRfc: true, useReceiverAccount: true, useReceiverAccountRegExp: /^S*$/, allowPaymentSignature: true }]; const keys = map.map(entry => entry.key); const index = keys.findIndex(keyEntry => keyEntry === key); if (index === -1) { throw new RangeError(`Key '${key}' was not found in the catalog`); } return new FormaPagoEntry(map[index]); } } class ValidatePagoException extends Error { constructor(...args) { super(...args); this._status = void 0; } getStatus() { return this._status || Status.error(); } setStatus(status) { this._status = status; return this; } } class AbstractPagoValidator { constructor() { this.code = ''; this.title = ''; } getCode() { return this.code; } getTitle() { return this.title; } registerInAssets(asserts) { asserts.put(this.getCode(), this.getTitle(), Status.ok()); } isGreaterThan(value, compare) { return value - compare > 0.0000001; } isEqual(expected, value) { return Math.abs(expected - value) < 0.0000001; } createCurrencyDecimals(currency) { try { return cfdiutilsCommon.CurrencyDecimals.newFromKnownCurrencies(currency); } catch (e) { return new cfdiutilsCommon.CurrencyDecimals(currency || 'XXX', 0); } } createPaymentType(paymentType) { try { return new FormaPagoCatalog().obtain(paymentType); } catch (e) { throw new ValidatePagoException(`La forma de pago "${paymentType}" no esta definida`); } } } /** * PAGO02: En un pago, la fecha debe cumplir con el formato específico */ class Fecha extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO02'; this.title = 'En un pago la fecha debe cumplir con el formato específico'; } validatePago(pago) { if (!AssertFechaFormat.hasFormat(pago.get('FechaPago'))) { throw new ValidatePagoException(`FechaPago: "${pago.get('FechaPago')}"`); } return true; } } /** * PAGO03: En un pago, la forma de pago debe existir y no puede ser "99" (CRP201) */ class FormaDePago extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO03'; this.title = 'En un pago, la forma de pago debe existir y no puede ser "99" (CRP201)'; } validatePago(pago) { try { const paymentType = this.createPaymentType(pago.get('FormaDePagoP')); if ('99' === paymentType.key()) { throw new ValidatePagoException('Cannot be 99'); } } catch (e) { throw new ValidatePagoException(`FormaDePagoP: "${pago.get('FormaDePagoP')}" ${e.message}`); } return true; } } /** * PAGO04: En un pago, la moneda debe existir y no puede ser "XXX" (CRP202) */ class MonedaPago extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO04'; this.title = 'En un pago, la moneda debe existir y no puede ser "XXX" (CRP202)'; } validatePago(pago) { if ('' === pago.get('MonedaP') || 'XXX' === pago.get('MonedaP')) { throw new ValidatePagoException(`Moneda: "${pago.get('MonedaP')}"`); } return true; } } /** * PAGO05: En un pago, cuando la moneda no es "MXN" no debe existir tipo de cambio, * de lo contrario el tipo de cambio debe existir (CRP203, CRP204) */ class TipoCambioExists extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO05'; this.title = ['En un pago, cuando la moneda no es "MXN" no debe existir tipo de cambio,', ' de lo contrario el tipo de cambio debe existir (CRP203, CRP204)'].join(''); } validatePago(pago) { if (!('MXN' === pago.get('MonedaP') ? '' === pago.get('TipoCambioP') : '' !== pago.get('TipoCambioP'))) { throw new ValidatePagoException(`Moneda "${pago.get('MonedaP')}", Tipo de cambio: "${pago.get('TipoCambioP')}"`); } return true; } } /** * PAGO06: En un pago, el tipo de cambio debe ser numérico, no debe exceder 6 decimales y debe ser mayor a "0.000001" */ class TipoCambioValue extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO06'; this.title = ['En un pago, el tipo de cambio debe ser numérico,', ' no debe exceder 6 decimales y debe ser mayor a "0.000001"'].join(''); } validatePago(pago) { if (!pago.offsetExists('TipoCambioP')) { return true; } let reason = ''; if (isNaN(Number(pago.get('TipoCambioP')))) { reason = 'No es numérico'; } else if (cfdiutilsCommon.CurrencyDecimals.decimalsCount(pago.get('TipoCambioP')) > 6) { reason = 'Tiene más de 6 decimales'; } else if (!this.isGreaterThan(parseFloat(pago.get('TipoCambioP') || '0'), 0.000001)) { reason = 'No es mayor a "0.000001"'; } if ('' !== reason) { throw new ValidatePagoException(`TipoCambioP: ${pago.get('TipoCambioP')}, ${reason}`); } return true; } } /** * PAGO07: En un pago, el monto debe ser mayor a cero (CRP207) */ class MontoGreaterThanZero extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO07'; this.title = 'En un pago, el monto debe ser mayor a cero (CRP207)'; } validatePago(pago) { if (!this.isGreaterThan(parseFloat(pago.get('Monto') || '0'), 0)) { throw new ValidatePagoException(`Monto: "${pago.get('Monto')}"`); } return true; } } /** * PAGO08: En un pago, el monto debe tener hasta la cantidad de decimales que soporte la moneda (CRP208) */ class MontoDecimals extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO08'; this.title = 'En un pago, el monto debe tener hasta la cantidad de decimales que soporte la moneda (CRP208)'; } validatePago(pago) { const currency = this.createCurrencyDecimals(pago.get('MonedaP')); if (!currency.doesNotExceedDecimals(pago.get('Monto'))) { throw new ValidatePagoException(`Monto: "${pago.get('Monto')}", MaxDecimals: "${currency.decimals()}"`); } return true; } } class CalculateDocumentAmountTrait { calculateDocumentAmount(doctoRelacionado, pago) { // el importe pagado es el que está en el documento if (doctoRelacionado.offsetExists('ImpPagado')) { return Number.parseFloat(doctoRelacionado.get('ImpPagado')); } // el importe pagado es el que está en el pago const doctosCount = pago.searchNodes('pago10:DoctoRelacionado').length; if (1 === doctosCount && !doctoRelacionado.offsetExists('TipoCambioDR')) { return Number.parseFloat(pago.get('Monto')); } // no hay importe pagado return 0.0; } } /** * PAGO09: En un pago, el monto del pago debe encontrarse entre límites mínimo y máximo de la suma * de los valores registrados en el importe pagado de los documentos relacionados (Guía llenado) */ class MontoBetweenIntervalSumOfDocuments extends tsMixer.Mixin(AbstractPagoValidator, CalculateDocumentAmountTrait) { constructor(...args) { super(...args); this.code = 'PAGO09'; this.title = ['En un pago, el monto del pago debe encontrarse entre límites mínimo y máximo de la suma', ' de los valores registrados en el importe pagado de los documentos relacionados (Guía llenado)'].join(''); } validatePago(pago) { const pagoAmount = parseFloat(pago.get('Monto') || '0'); const bounds = this.calculateDocumentsAmountBounds(pago); const currencyDecimals = cfdiutilsCommon.CurrencyDecimals.newFromKnownCurrencies(pago.get('MonedaP'), 2); const lower = currencyDecimals.round(bounds['lower']); const upper = currencyDecimals.round(bounds['upper']); if (pagoAmount < lower || pagoAmount > upper) { throw new ValidatePagoException(`Monto del pago: "${pagoAmount}", Suma mínima: "${lower}", Suma máxima: "${upper}"`); } return true; } calculateDocumentsAmountBounds(pago) { const documents = pago.searchNodes('pago10:DoctoRelacionado'); const values = documents.map(document => this.calculateDocumentAmountBounds(document, pago)); return { lower: values.reduce((a, b) => a + b['lower'], 0), upper: values.reduce((a, b) => a + b['upper'], 0) }; } calculateDocumentAmountBounds(doctoRelacionado, pago) { const amount = this.calculateDocumentAmount(doctoRelacionado, pago); const impPagado = doctoRelacionado.get('ImpPagado') || amount; const tipoCambioDR = doctoRelacionado.get('TipoCambioDR'); let exchangeRate = 1; if ('' !== tipoCambioDR && pago.get('MonedaP') !== pago.get('MonedaDR')) { exchangeRate = parseFloat(tipoCambioDR); } const numDecimalsAmount = this.getNumDecimals(`${impPagado}`); const numDecimalsExchangeRate = this.getNumDecimals(`${tipoCambioDR}`); if (0 === numDecimalsExchangeRate) { return { lower: amount / exchangeRate, upper: amount / exchangeRate }; } const almostTwo = 2 - 10 ** -10; const lowerAmount = amount - 10 ** -numDecimalsAmount / 2; const lowerExchangeRate = exchangeRate + 10 ** -numDecimalsExchangeRate / almostTwo; const upperAmount = amount + 10 ** -numDecimalsAmount / almostTwo; const upperExchangeRate = exchangeRate - 10 ** -numDecimalsExchangeRate / 2; return { lower: lowerAmount / lowerExchangeRate, upper: upperAmount / upperExchangeRate }; } getNumDecimals(numeric) { if (isNaN(Number(numeric)) || isNaN(parseFloat(numeric))) { return 0; } const pointPosition = numeric.indexOf('.'); if (pointPosition === -1) { return 0; } return numeric.length - 1 - pointPosition; } } /** * PAGO10: En un pago, cuando el RFC del banco emisor de la cuenta ordenante existe * debe ser válido y diferente de "XAXX010101000" */ class BancoOrdenanteRfcCorrecto extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO10'; this.title = ['En en pago, cuando el RFC del banco emisor de la cuenta ordenante existe', ' debe ser válido y diferente de "XAXX010101000"'].join(''); } validatePago(pago) { if (pago.offsetExists('RfcEmisorCtaOrd')) { try { rfc.Rfc.checkIsValid(pago.get('RfcEmisorCtaOrd'), rfc.Rfc.DISALLOW_GENERIC); } catch (e) { throw new ValidatePagoException(e.message); } } return true; } } /** * PAGO11: En un pago, cuando el RFC del banco emisor sea "XEXX010101000" el nombre del banco es requerido (CRP211) */ class BancoOrdenanteNombreRequerido extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO11'; this.title = ['En un pago, cuando el RFC del banco emisor sea "XEXX010101000"', ' el nombre del banco es requerido (CRP211)'].join(''); } validatePago(pago) { if (rfc.Rfc.RFC_FOREIGN === pago.get('RfcEmisorCtaOrd') && '' === pago.get('NomBancoOrdExt')) { throw new ValidatePagoException(`Rfc: "${pago.get('RfcEmisorCtaOrd')}", Nombre "${pago.get('NomBancoOrdExt')}"`); } return true; } } /** * PAGO12: En un pago, cuando la forma de pago no sea bancarizada el RFC del banco emisor no debe existir (CRP238) */ class BancoOrdenanteRfcProhibido extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO12'; this.title = ['En un pago, cuando la forma de pago no sea bancarizada', ' el RFC del banco emisor no debe existir (CRP238)'].join(''); } validatePago(pago) { if ('' === pago.get('FormaDePagoP')) { throw new ValidatePagoException('No está establecida la forma de pago'); } const payment = this.createPaymentType(pago.get('FormaDePagoP')); // si NO es bancarizado y está establecido el RFC del Emisor de la cuenta ordenante if (!payment.allowSenderRfc() && pago.offsetExists('RfcEmisorCtaOrd')) { throw new ValidatePagoException(`Bancarizado: Si, Rfc: "${pago.get('RfcEmisorCtaOrd')}"`); } return true; } } /** * PAGO13: En un pago, cuando la forma de pago no sea bancarizada la cuenta ordenante no debe existir (CRP212) */ class CuentaOrdenanteProhibida extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO13'; this.title = ['En un pago, cuando la forma de pago no sea bancarizada', ' la cuenta ordenante no debe existir (CRP212)'].join(''); } validatePago(pago) { const payment = this.createPaymentType(pago.get('FormaDePagoP')); // si NO es bancarizado y está establecida la cuenta ordenante existe if (!payment.allowSenderAccount() && pago.offsetExists('CtaOrdenante')) { throw new ValidatePagoException(`Bancarizado: Si, Cuenta: "${pago.get('CtaOrdenante')}"`); } return true; } } /** * PAGO14: En un pago, cuando la cuenta ordenante existe debe cumplir con su patrón específico (CRP213) */ class CuentaOrdenantePatron extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO14'; this.title = 'En un pago, cuando la cuenta ordenante existe debe cumplir con su patrón específico (CRP213)'; } validatePago(pago) { // Solo validar si está establecida la cuenta ordenante if (pago.offsetExists('CtaOrdenante')) { const payment = this.createPaymentType(pago.get('FormaDePagoP')); const pattern = payment.senderAccountPattern(); if (!pago.get('CtaOrdenante').match(pattern)) { throw new ValidatePagoException(`Cuenta: "${pago.get('CtaOrdenante')}". Patrón "${pattern}"`); } } return true; } } /** * PAGO15: En un pago, cuando el RFC del banco emisor de la cuenta beneficiaria existe * debe ser válido y diferente de "XAXX010101000" */ class BancoBeneficiarioRfcCorrecto extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO15'; this.title = ['En un pago, cuando el RFC del banco emisor de la cuenta beneficiaria existe', ' debe ser válido y diferente de "XAXX010101000"'].join(''); } validatePago(pago) { if (pago.offsetExists('RfcEmisorCtaBen')) { try { rfc.Rfc.checkIsValid(pago.get('RfcEmisorCtaBen'), rfc.Rfc.DISALLOW_GENERIC); } catch (e) { throw new ValidatePagoException(e.message); } } return true; } } /** * PAGO16: En un pago, cuando la forma de pago no sea 02, 03, 04, 05, 28, 29 o 99 * el RFC del banco de la cuenta beneficiaria no debe existir (CRP214) */ class BancoBeneficiarioRfcProhibido extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO16'; this.title = ['En un pago, cuando la forma de pago no sea 02, 03, 04, 05, 28, 29 o 99', ' el RFC del banco de la cuenta beneficiaria no debe existir (CRP214)'].join(''); } validatePago(pago) { const payment = this.createPaymentType(pago.get('FormaDePagoP')); if (!payment.allowReceiverRfc() && pago.offsetExists('RfcEmisorCtaBen')) { throw new ValidatePagoException(`FormaDePago: "${pago.get('FormaDePagoP')}", Rfc: "${pago.get('RfcEmisorCtaBen')}"`); } return true; } } /** * PAGO17: En un pago, cuando la forma de pago no sea 02, 03, 04, 05, 28, 29 o 99 * la cuenta beneficiaria no debe existir (CRP215) */ class CuentaBeneficiariaProhibida extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO17'; this.title = ['En un pago, cuando la forma de pago no sea 02, 03, 04, 05, 28, 29 o 99', ' la cuenta beneficiaria no debe existir (CRP215)'].join(''); } validatePago(pago) { const payment = this.createPaymentType(pago.get('FormaDePagoP')); // si No es bancarizado y está establecida la cuenta beneficiaria if (!payment.allowReceiverAccount() && pago.offsetExists('CtaBeneficiario')) { throw new ValidatePagoException(`Forma de pago: "${pago.get('FormaDePagoP')}", Cuenta: "${pago.get('CtaBeneficiario')}"`); } return true; } } /** * PAGO18: En un pago, cuando la cuenta beneficiaria existe debe cumplir con su patrón específico (CRP239) */ class CuentaBeneficiariaPatron extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO18'; this.title = ['En un pago, cuando la cuenta beneficiaria existe', ' debe cumplir con su patrón específico (CRP213)'].join(''); } validatePago(pago) { // solo validar si está establecida la cuenta ordenante if (pago.offsetExists('CtaBeneficiario')) { const payment = this.createPaymentType(pago.get('FormaDePagoP')); const pattern = payment.receiverAccountPattern(); if (!pago.get('CtaBeneficiario').match(pattern)) { throw new ValidatePagoException(`Cuenta: "${pago.get('CtaOrdenante')}". Patrón: "${pattern}"`); } } return true; } } /** * PAGO19: En un pago, cuando la forma de pago no sea 03 o 99 el tipo de cadena de pago no debe existir (CRP216) */ class TipoCadenaPagoProhibido extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO19'; this.title = ['En un pago, cuando la forma de pago no sea 03 o 99', ' el tipo de cadena de pago no debe existir (CRP216)'].join(''); } validatePago(pago) { const payment = this.createPaymentType(pago.get('FormaDePagoP')); // si NO es bancarizado y está establecida la cuenta ordenante existe if (!payment.allowPaymentSignature() && pago.offsetExists('TipoCadPago')) { throw new ValidatePagoException(`Forma de pago: "${pago.get('FormaDePagoP')}", Tipo cadena pago: "${pago.get('TipoCadPago')}"`); } return true; } } /** * PAGO20: En un pago, si existe el tipo de cadena de pago debe existir * el certificado del pago y viceversa (CRP227 y CRP228) */ class TipoCadenaPagoCertificado extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO20'; this.title = ['En un pago, si existe el tipo de cadena de pago debe existir', ' el certificado del pago y viceversa (CRP227 y CRP228)'].join(''); } validatePago(pago) { const notEmpty = '' !== pago.get('TipoCadPago') ? '' === pago.get('CertPago') : '' !== pago.get('CertPago'); if (notEmpty || (pago.offsetExists('TipoCadPago') ? !pago.offsetExists('CertPago') : pago.offsetExists('CertPago'))) { throw new ValidatePagoException(`Tipo cadena pago: "${pago.get('TipoCadPago')}", Certificado: "${pago.get('CertPago')}"`); } return true; } } /** * PAGO21: En un pago, si existe el tipo de cadena de pago debe existir * la cadena del pago y viceversa (CRP229 y CRP230) */ class TipoCadenaPagoCadena extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO21'; this.title = ['En un pago, si existe el tipo de cadena de pago debe existir', ' la cadena del pago y viceversa (CRP229 y CRP230)'].join(''); } validatePago(pago) { const notEmpty = '' !== pago.get('TipoCadPago') ? '' === pago.get('CadPago') : '' !== pago.get('CadPago'); if (notEmpty || (pago.offsetExists('TipoCadPago') ? !pago.offsetExists('CadPago') : pago.offsetExists('CadPago'))) { throw new ValidatePagoException(`Tipo cadena pago: "${pago.get('TipoCadPago')}", Cadena: "${pago.get('CadPago')}"`); } return true; } } /** * PAGO22: En un pago, si existe el tipo de cadena de pago debe existir * el sello del pago y viceversa (CRP231 y CRP232) */ class TipoCadenaPagoSello extends AbstractPagoValidator { constructor(...args) { super(...args); this.code = 'PAGO22'; this.title = ['En un pago, si existe el tipo de cadena de pago debe existir', ' el sello del pago y viceversa (CRP231 y CRP232)'].join(''); } validatePago(pago) { const notEmpty = '' !== pago.get('TipoCadPago') ? '' === pago.get('SelloPago') : '' !== pago.get('SelloPago'); if (notEmpty || (pago.offsetExists('TipoCadPago') ? !pago.offsetExists('SelloPago') : pago.offsetExists('SelloPago'))) { throw new ValidatePagoException(`Tipo cadena pago: "${pago.get('TipoCadPago')}", Sello: "${pago.get('SelloPago')}"`); } return true; } } class ValidateDoctoException extends ValidatePagoException { constructor(...args) { super(...args); this._index = void 0; this._validatorCode = void 0; } setIndex(index) { this._index = index; return this; } setValidatorCode(validatorCode) { this._validatorCode = validatorCode; return this; } getIndex() { return this._index; } getValidatorCode() { return this._validatorCode; } } class AbstractDoctoRelacionadoValidator extends AbstractPagoValidator { constructor(...args) { super(...args); this._pago = void 0; this._index = void 0; } exception(message) { const exception = new ValidateDoctoException(message); exception.setIndex(this.getIndex()); exception.setValidatorCode(this.getCode()); return exception; } validatePago(_pago) { throw new Error('This method must not be called'); } getPago() { return this._pago; } setPago(pago) { this._pago = pago; } getIndex() { return this._index; } setIndex(index) { this._index = index; } } /** * PAGO23: En un documento relacionado, la moneda no puede ser "XXX" (CRP217) */ class Moneda extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO23'; this.title = 'En un documento relacionado, la moneda no puede ser "XXX" (CRP217)'; } validateDoctoRelacionado(docto) { if ('XXX' === docto.get('MonedaDR')) { throw this.exception(`MonedaDR: "${docto.get('MonedaDR')}"`); } return true; } } /** * PAGO24: En un documento relacionado, el tipo de cambio debe existir cuando la moneda del pago * es diferente a la moneda del documento y viceversa (CRP218, CRP219) */ class TipoCambioRequerido extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO24'; this.title = ['En un documento relacionado, el tipo de cambio debe existir cuando la moneda del pago', ' es diferente a la moneda del documento y viceversa (CRP218, CRP219)'].join(''); } validateDoctoRelacionado(docto) { const pago = this.getPago(); const currencyIsEqual = pago.get('MonedaP') === docto.get('MonedaDR'); if (!(currencyIsEqual ? !docto.offsetExists('TipoCambioDR') : docto.offsetExists('TipoCambioDR'))) { throw this.exception(`Moneda pago: "${pago.get('MonedaP')}", Moneda documento: "${docto.get('MonedaDR')}", Tipo cambio docto: "${docto.get('TipoCambioDR')}"`); } return true; } } /** * PAGO25: En un documento relacionado, el tipo de cambio debe tener el valor "1" * cuando la moneda del documento es MXN y diferente de la moneda del pago (CRP220) */ class TipoCambioValor extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO25'; this.title = ['En un documento relacionado, el tipo de cambio debe tener el valor "1"', ' cuando la moneda del documento es MXN y diferente de la moneda del pago (CRP220)'].join(''); } validateDoctoRelacionado(docto) { const pago = this.getPago(); if ('MXN' === docto.get('MonedaDR') && pago.get('MonedaP') !== docto.get('MonedaDR') && '1' !== docto.get('TipoCambioDR')) { throw this.exception(`Moneda pago: "${pago.get('MonedaP')}", Moneda documento: "${docto.get('MonedaDR')}", Tipo cambio docto: "${docto.get('TipoCambioDR')}"`); } return true; } } /** * PAGO26: En un documento relacionado, el importe del saldo anterior debe ser mayor a cero (CRP221) */ class ImporteSaldoAnteriorValor extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO26'; this.title = 'En un documento relacionado, el importe del saldo anterior debe ser mayor a cero (CRP221)'; } validateDoctoRelacionado(docto) { const value = parseFloat(docto.get('ImpSaldoAnt') || '0'); if (!this.isGreaterThan(value, 0)) { throw this.exception(`ImpSaldoAnt: ${docto.get('ImpSaldoAnt')}`); } return true; } } /** * PAGO27: En un documento relacionado, el importe pagado debes ser mayor a cero (CRP223) */ class ImportePagadoValor extends tsMixer.Mixin(AbstractDoctoRelacionadoValidator, CalculateDocumentAmountTrait) { constructor(...args) { super(...args); this.code = 'PAGO27'; this.title = 'En un documento relacionado, el importe pagado debe ser mayor a cero (CRP223)'; } validateDoctoRelacionado(docto) { let value; if (docto.offsetExists('ImpPagado')) { value = parseFloat(docto.attributes().get('ImpPagado') || '0'); } else { value = this.calculateDocumentAmount(docto, this.getPago()); } if (!this.isGreaterThan(value, 0)) { throw this.exception(`ImpPagado: ${docto.get('ImpPagado')}, valor: ${value}`); } return true; } } /** * PAGO28: En un documento relacionado, el importe del saldo insoluto debe ser mayor o igual a cero * e igual a la resta del importe del saldo anterior menos el importe pagado (CRP226) */ class ImporteSaldoInsolutoValor extends tsMixer.Mixin(AbstractDoctoRelacionadoValidator, CalculateDocumentAmountTrait) { constructor(...args) { super(...args); this.code = 'PAGO28'; this.title = ['En un documento relacionado, el importe del saldo insoluto debe ser mayor o igual a cero', ' e igual a la resta del importe del saldo anterior menos el importe pagado (CRP226)'].join(''); } validateDoctoRelacionado(docto) { const value = parseFloat(docto.get('ImpSaldoInsoluto') || '0'); if (!this.isEqual(0, value) && !this.isGreaterThan(value, 0)) { throw this.exception(`ImpSaldoInsoluto: ${docto.get('ImpSaldoInsoluto')}`); } const expected = parseFloat(docto.get('ImpSaldoAnt') || '0') - this.calculateDocumentAmount(docto, this.getPago()); if (!this.isEqual(expected, value)) { throw this.exception(`ImpSaldoInsoluto: ${docto.get('ImpSaldoInsoluto')}, Esperado: ${expected}`); } return true; } } /** * PAGO29: En un documento relacionado, los importes de importe pagado, saldo anterior y saldo insoluto * deben tener hasta la cantidad de decimales que soporte la moneda (CRP222, CRP224, CRP225) */ class ImportesDecimales extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO29'; this.title = ['En un documento relacionado, los importes de importe pagado, saldo anterior y saldo insoluto', ' deben tener hasta la cantidad de decimales que soporte la moneda (CRP222, CRP224, CRP225)'].join(''); } validateDoctoRelacionado(docto) { const currency = this.createCurrencyDecimals(docto.get('MonedaDR')); if (!currency.doesNotExceedDecimals(docto.get('ImpSaldoAnt') || '0')) { throw this.exception(`ImpSaldoAnt "${docto.get('ImpSaldoAnt')}", Decimales: ${currency.decimals()}`); } if (docto.offsetExists('ImpPagado') && !currency.doesNotExceedDecimals(docto.get('ImpPagado') || '0')) { throw this.exception(`ImpPagado: "${docto.get('ImpPagado')}", Decimales: ${currency.decimals()}`); } if (!currency.doesNotExceedDecimals(docto.get('ImpSaldoInsoluto') || '0')) { throw this.exception(`ImpSaldoInsoluto: "${docto.get('ImpSaldoInsoluto')}", Decimales: ${currency.decimals()}`); } return true; } } /** * PAGO30: En un documento relacionado, el importe pagado es requerido cuando * el tipo de cambio existe o existe más de un documento relacionado (CRP235) */ class ImportePagadoRequerido extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO30'; this.title = ['En un documento relacionado, el importe pagado es requerido cuando', ' el tipo de cambio existe o existe más de un documento relacionado (CRP235)'].join(''); } validateDoctoRelacionado(docto) { if (!docto.offsetExists('ImpPagado')) { const documentsCount = this.getPago().searchNodes('pago10:DoctoRelacionado').length; if (documentsCount > 1) { throw this.exception('No hay importe pagado y hay más de 1 documento en el pago'); } if (docto.offsetExists('TipoCambioDR')) { throw this.exception('No hay importe pagado y existe el tipo de cambio del documento'); } } return true; } } /** * PAGO31: En un documento relacionado, el número de parcialidad es requerido cuando * el tipo de cambio existe o existe más de un documento relacionado (CRP234) */ class NumeroParcialidadRequerido extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO31'; this.title = ['En un documento relacionado, el número de parcialidad es requerido cuando', ' el tipo de cambio existe o existe más de un documento relacionado (CRP233)'].join(''); } validateDoctoRelacionado(docto) { if (!docto.offsetExists('NumParcialidad') && 'PPD' === docto.get('MetodoDePagoDR')) { throw this.exception('No hay número de parcialidad y el método de pago es PPD'); } return true; } } /** * PAGO32: En un documento relacionado, el saldo anterior es requerido cuando * el tipo de cambio existe o existe más de un documento relacionado (CRP234) */ class ImporteSaldoAnteriorRequerido extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO32'; this.title = ['En un documento relacionado, el saldo anterior es requerido cuando', ' el tipo de cambio existe o existe más de un documento relacionado (CRP234)'].join(''); } validateDoctoRelacionado(docto) { if (!docto.offsetExists('ImpSaldoAnt') && 'PPD' === docto.get('MetodoDePagoDR')) { throw this.exception('No hay saldo anterior y el método de pago es PPD'); } return true; } } /** * PAGO33: En un documento relacionado, el saldo insoluto es requerido cuando * el tipo de cambio existe o existe más de un documento relacionado (CRP234) */ class ImporteSaldoInsolutoRequerido extends AbstractDoctoRelacionadoValidator { constructor(...args) { super(...args); this.code = 'PAGO33'; this.title = ['En un documento relacionado, el saldo insoluto es requerido cuando', ' el tipo de cambio existe o existe más de un documento relacionado (CRP233)'].join(''); } validateDoctoRelacionado(docto) { if (!docto.offsetExists('ImpSaldoInsoluto') && 'PPD' === docto.get('MetodoDePagoDR')) { throw this.exception('No hay saldo insoluto y el método de pago es PPD'); } return true; } } class DoctoRelacionado extends AbstractPagoValidator { constructor() { super(); this.validators = void 0; this.validators = this.createValidators(); } getValidators() { return this.validators; } createValidators() { return [new Moneda(), new TipoCambioRequerido(), new TipoCambioValor(), new ImporteSaldoAnteriorValor(), new ImportePagadoValor(), new ImporteSaldoInsolutoValor(), new ImportesDecimales(), new ImportePagadoRequerido(), new NumeroParcialidadRequerido(), new ImporteSaldoAnteriorRequerido(), new ImporteSaldoInsolutoRequerido() // PAGO33 ]; } // override registerInAssets to add validators instead of itself registerInAssets(asserts) { this.validators.forEach(validator => { validator.registerInAssets(asserts); }); } validatePago(pago) { // when validate pago perform validators on all documents const validators = this.getValidators(); pago.searchNodes('pago10:DoctoRelacionado').forEach((doctoRelacionado, index) => { validators.forEach(validator => { validator.setPago(pago); validator.setIndex(index); validator.validateDoctoRelacionado(doctoRelacionado); }); }); return true; } } /** * PAGO30: En un pago, la suma de los valores registrados o predeterminados en el importe pagado * de los documentos relacionados debe ser menor o igual que el monto del pago (CRP206) */ class MontoGreaterOrEqualThanSumOfDocuments extends tsMixer.Mixin(AbstractPagoValidator, CalculateDocumentAmountTrait) { constructor(...args) { super(...args); this.code = 'PAGO30'; this.title = ['En un pago, la suma de los valores registrados o predeterminados en el importe pagado', ' de los documentos relacionados debe ser menor o igual que el monto del pago (CRP206)'].join(''); } validatePago(pago) { const currency = this.createCurrencyDecimals(pago.get('MonedaP')); const sumOfDocuments = this.calculateSumOfDocuments(pago, currency); const pagoAmount = parseFloat(pago.get('Monto') || '0'); if (this.isGreaterThan(sumOfDocuments, pagoAmount)) { throw new ValidatePagoException(`Monto del pago: "${pagoAmount}", Suma de documentos: "${sumOfDocuments}"`); } return true; } calculateSumOfDocuments(pago, currency) { let sumOfDocuments = 0; const documents = pago.searchNodes('pago10:DoctoRelacionado'); documents.forEach(document => { let exchangeRate = parseFloat(document.get('TipoCambioDR') || '0'); if (this.isEqual(exchangeRate, 0)) { exchangeRate = 1; } sumOfDocuments += currency.round(this.calculateDocumentAmount(document, pago) / exchangeRate); }); return sumOfDocuments; } } /** * Pago - Válida los nodos de pago dentro del complemento de pagos * * Se generan mensajes de error en los pagos con clave: * PAGO??-XX donde '??', es el número de validación general y XX es el número del nodo con problemas * * Se generan mensajes de error en los documentos relacionados con clave: * PAGO??-XX-YY donde YY es el número del nodo con problemas */ class Pago extends AbstractRecepcionPagos10 { constructor(...args) { super(...args); this._asserts = new Asserts(); this._validators = null; } createValidators() { return [new Fecha(), new FormaDePago(), new MonedaPago(), new TipoCambioExists(), new TipoCambioValue(), new MontoGreaterThanZero(), new MontoDecimals(), new MontoBetweenIntervalSumOfDocuments(), new BancoOrdenanteRfcCorrecto(), new BancoOrdenanteNombreRequerido(), new BancoOrdenanteRfcProhibido(), new CuentaOrdenanteProhibida(), new CuentaOrdenantePatron(), new BancoBeneficiarioRfcCorrecto(), new BancoBeneficiarioRfcProhibido(), new CuentaBeneficiariaProhibida(), new CuentaBeneficiariaPatron(), new TipoCadenaPagoProhibido(), new TipoCadenaPagoCertificado(), new TipoCadenaPagoCadena(), new TipoCadenaPagoSello(), new DoctoRelacionado(), new MontoGreaterOrEqualThanSumOfDocuments() // PAGO30 ]; } getValidators() { if (!this._validators) { this._validators = this.createValidators(); } return this._validators; } validateRecepcionPagos(comprobante, asserts) { this._asserts = asserts; // create pago validators array const validators = this.createValidators(); // register pago validators array into asserts validators.forEach(validator => { validator.registerInAssets(asserts); }); // obtain the pago nodes const pagoNodes = comprobante.searchNodes('cfdi:Complemento', 'pago10:Pagos', 'pago10:Pago'); pagoNodes.forEach((pagoNode, index) => { // pass each pago node throw validators validators.forEach(validator => { try { if (!validator.validatePago(pagoNode)) { throw new Error(`The validation of pago ${index} ${validator.constructor.name} return false`); } } catch (e) { if (e instanceof ValidateDoctoException) { this.setDoctoRelacionadoStatus(e.getValidatorCode(), index, e.getIndex(), e.getStatus(), e.message); } else if (e instanceof ValidatePagoException) { this.setPagoStatus(validator.getCode(), index, e.getStatus(), e.message); } } }); }); return Promise.resolve(); } setPagoStatus(code, index, errorStatus, explanation = '') { const assert = this._asserts.get(code); assert.setStatus(errorStatus); this._asserts.put(`${assert.getCode()}-${index.toString().padStart(2, '0')}`, assert.getTitle(), errorStatus, explanation); } setDoctoRelacionadoStatus(code, pagoIndex, doctoIndex, errorStatus, explanation = '') { const assert = this._asserts.get(code); const doctoCode = `${assert.getCode()}-${pagoIndex.toString().padStart(2, '0')}-${doctoIndex.toString().padStart(2, '0')}`; this.setPagoStatus(code, pagoIndex, errorStatus); this._asserts.put(doctoCode, assert.getTitle(), errorStatus, explanation); } } /** * Pagos - Válida el contenido del nodo del complemento de pago * * - PAGOS01: Debe existir al menos un pago en el complemento de pagos */ class Pagos extends AbstractRecepcionPagos10 { validateRecepcionPagos(comprobante, asserts) { const assert = asserts.put('PAGOS01', 'Debe existir al menos un pago en el complemento de pagos'); const pagoCollection = comprobante.searchNodes('cfdi:Complemento', 'pago10:Pagos', 'pago10:Pago'); assert.setStatus(Status.when(pagoCollection.length > 0), 'Debe existir al menos un pago en el complemento de pagos'); return Promise.resolve(); } } /** * UsoCfdi * * - PAGUSO01: El uso del CFDI debe ser "P01" (CRP110) */ class UsoCfdi extends AbstractRecepcionPagos10 { validateRecepcionPagos(comprobante, asserts) { const assert = asserts.put('PAGUSO01', 'El uso del CFDI debe ser "P01" (CRP110)'); const receptor = comprobante.searchNode('cfdi:Receptor'); if (!receptor) { assert.setStatus(Status.error(), 'No se encontró el nodo Receptor'); return Promise.resolve(); } assert.setStatus(Status.when('P01' === receptor.get('UsoCFDI')), `Uso CFDI: "${receptor.get('UsoCFDI')}"`); return Promise.resolve(); } } /** * SelloDigitalCertificado * * Válida que: * - SELLO01: Se puede obtener el certificado del comprobante * - SELLO02: El número de certificado del comprobante igual al encontrado en el certificado * - SELLO03: El RFC del comprobante igual al encontrado en el certificado * - SELLO04: El nombre del emisor del comprobante es igual al encontrado en el certificado * - SELLO05: La fecha del documento es mayor o igual a la fecha de inicio de vigencia del certificado * - SELLO06: La fecha del documento menor o igual a la fecha de fin de vigencia del certificado * - SELLO07: El sello del comprobante está en base 64 * - SELLO08: El sello del comprobante coincide con el certificado y la cadena de origen generada */ class SelloDigitalCertificado extends tsMixer.Mixin(AbstractDiscoverableVersion40, SelloDigitalCertificadoValidatorTrait) {} var selloDigitalCertificado = { __proto__: null, SelloDigitalCertificado: SelloDigitalCertificado }; /** * TimbreFiscalDigitalSello * * Válida que: * - TFDSELLO01: El Sello SAT del Timbre Fiscal Digital corresponde al certificado SAT */ class TimbreFiscalDigitalSello extends tsMixer.Mixin(AbstractDiscoverableVersion40, TimbreFiscalDigitalSelloValidatorTrait) {} var timbreFiscalDigitalSello = { __proto__: null, TimbreFiscalDigitalSello: TimbreFiscalDigitalSello }; /** * TimbreFiscalDigitalVersion * * Válida que: * - TFDVERSION01: Si existe el complemento timbre fiscal digital, entonces su versión debe ser 1.1 */ class TimbreFiscalDigitalVersion extends tsMixer.Mixin(AbstractDiscoverableVersion40, TimbreFiscalDigitalVersionValidatorTrait) {} var timbreFiscalDigitalVersion = { __proto__: null, TimbreFiscalDigitalVersion: TimbreFiscalDigitalVersion }; class Hydrater extends tsMixer.Mixin(cfdiutilsCore.XmlResolverPropertyTrait, XmlStringPropertyTrait, cfdiutilsCore.XsltBuilderPropertyTrait) { hydrate(validator) { if (this.isRequireXmlStringInterface(validator)) { validator.setXmlString(this.getXmlString()); } if (this.hasXmlResolver() && this.isRequireXmlResolverInterface(validator)) { validator.setXmlResolver(this.getXmlResolver()); } if (this.isRequireXsltBuilderInterface(validator)) { validator.setXsltBuilder(this.getXsltBuilder()); } } isRequireXmlStringInterface(object) { const instance = object; return instance.setXmlString !== undefined && instance.getXmlString !== undefined; } isRequireXmlResolverInterface(object) { const instance = object; return instance.hasXmlResolver !== undefined && instance.getXmlResolver !== undefined && instance.setXmlResolver !== undefined; } isRequireXsltBuilderInterface(object) { const instance = object; return instance.hasXsltBuilder !== undefined && instance.getXsltBuilder !== undefined && instance.setXsltBuilder !== undefined; } } class CfdiValidatorTrait extends tsMixer.Mixin(cfdiutilsCore.XsltBuilderPropertyTrait, cfdiutilsCore.XmlResolverPropertyTrait) { /** * Validate and return the asserts from the validation process. * This method can use a xml string and a CNodeInterface, * is your responsability that the node is the representation of the content. * * @param xmlString - * @param node - */ async validate(xmlString, node) { if ('' === xmlString) { throw new Error('The xml string to validate cannot be empty'); } const validator = this.createVersionedMultiValidator(); const hydrater = new Hydrater(); hydrater.setXmlString(xmlString); hydrater.setXmlResolver(this.hasXmlResolver() ? this.getXmlResolver() : null); hydrater.setXsltBuilder(this.getXsltBuilder()); validator.hydrate(hydrater); const asserts = new Asserts(); await validator.validate(node, asserts); return asserts; } /** * Validate and return the asserts from the validation process based on a xml string * * @param xmlString - */ validateXml(xmlString) { return this.validate(xmlString, cfdiutilsCommon.XmlNodeUtils.nodeFromXmlString(xmlString)); } /** * Validate and return the asserts from the validation process based on a node interface object * * @param node - */ validateNode(node) { return this.validate(cfdiutilsCommon.XmlNodeUtils.nodeToXmlString(node), node); } } let _Symbol$iterator; _Symbol$iterator = Symbol.iterator; class MultiValidator { get length() { return this._validators.length; } constructor(version) { this._validators = []; this._version = void 0; this._version = version; } getVersion() { return this._version; } async validate(comprobante, asserts) { for (const validator of this._validators) { if (!validator.canValidateCfdiVersion(this.getVersion())) { continue; } const localAsserts = new Asserts(); await validator.validate(comprobante, localAsserts); asserts.import(localAsserts); if (localAsserts.mustStop()) { break; } } return Promise.resolve(); } canValidateCfdiVersion(version) { return this._version === version; } hydrate(hydrater) { this._validators.forEach(validator => { hydrater.hydrate(validator); }); } /** * Collection methods */ add(validator) { this._validators.push(validator); } addMulti(...validators) { validators.forEach(validator => { this.add(validator); }); } exists(validator) { return this.indexOf(validator) >= 0; } indexOf(validator) { return this._validators.indexOf(validator); } remove(validator) { const index = this.indexOf(validator); if (index >= 0) { this._validators.splice(index, 1); } } removeAll() { this._validators = []; } // Iterators of MultiValidator [_Symbol$iterator]() { return this._validators[Symbol.iterator](); } } /** * XmlDefinition * * Válida que: * - XML01: El XML implementa el namespace %s con el prefijo cfdi * - XML02: El nodo principal se llama cfdi:Comprobante * - XML03: La versión es 4.0 */ class XmlDefinition extends AbstractDiscoverableVersion40 { validate(comprobante, asserts) { asserts.put('XML01', `El XML implementa el namespace ${XmlDefinition.CFDI40_NAMESPACE} con el prefijo cfdi.`, Status.when(XmlDefinition.CFDI40_NAMESPACE === comprobante.get('xmlns:cfdi')), `Valor de xmlns:cfdi: ${comprobante.get('xmlns:cfdi')}`); asserts.put('XML02', 'El nodo principal se llama cfdi:Comprobante', Status.when('cfdi:Comprobante' === comprobante.name()), `Nombre: ${comprobante.name()}`); asserts.put('XML03', 'La versión es 4.0', Status.when('4.0' === comprobante.get('Version')), `Version: ${comprobante.get('Version')}`); return Promise.resolve(); } } XmlDefinition.CFDI40_NAMESPACE = 'http://www.sat.gob.mx/cfd/4'; /** * SumasConceptosComprobanteImpuestos * * Esta clase genera la suma de subtotal, descuento, total e impuestos a partir de las sumas de los conceptos. * Con estas sumas válidas contra los valores del comprobante, los valores de impuestos * y la lista de impuestos trasladados y retenidos * * Válida que: * - SUMAS01: La suma de los importes de conceptos es igual al subtotal del comprobante * - SUMAS02: La suma de los descuentos es igual al descuento del comprobante * - SUMAS03: El cálculo del total es igual al total del comprobante * * - SUMAS04: El cálculo de impuestos trasladados es igual al total de impuestos trasladados * - SUMAS05: Todos los impuestos trasladados existen en el comprobante * - SUMAS06: Todos los valores de los impuestos trasladados conciden con el comprobante * - SUMAS07: No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado * * - SUMAS08: El cálculo de impuestos retenidos es igual al total de impuestos retenidos * - SUMAS09: Todos los impuestos retenidos existen en el comprobante * - SUMAS10: Todos los valores de los impuestos retenidos conciden con el comprobante * - SUMAS11: No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado * * - SUMAS12: El cálculo del descuento debe ser menor o igual al cálculo del subtotal * * - Adicionalmente, para SUMAS06 y SUMAS10 se generan: SUMASxx:yyy donde * - xx puede ser 06 o 10 * - yyy es el consecutivo de la línea del impuesto * - Se valida que El importe dek impuesto del Grupo X Impuesto X Tipo factor X Tasa o cuota X * es igual al importe del nodo */ class SumasConceptosComprobanteImpuestos extends AbstractDiscoverableVersion33 { constructor(...args) { super(...args); this._comprobante = void 0; this._asserts = void 0; this._sumasConceptos = void 0; } registerAsserts() { const asserts = { SUMAS01: 'La suma de los importes de conceptos es igual a el subtotal del comprobante', SUMAS02: 'La suma de los descuentos es igual a el descuento del comprobante', SUMAS03: 'El cálculo del total es igual a el total del comprobante', SUMAS04: 'El cálculo de impuestos trasladados es igual a el total de impuestos trasladados', SUMAS05: 'Todos los impuestos trasladados existen en el comprobante', SUMAS06: 'Todos los valores de los impuestos trasladados conciden con el comprobante', SUMAS07: 'No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado', SUMAS08: 'El cálculo de impuestos retenidos es igual a el total de impuestos retenidos', SUMAS09: 'Todos los impuestos retenidos existen en el comprobante', SUMAS10: 'Todos los valores de los impuestos retenidos conciden con el comprobante', SUMAS11: 'No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado', SUMAS12: 'El cálculo del descuento debe ser menor o igual al cálculo del subtotal' }; Object.entries(asserts).forEach(([code, title]) => { this._asserts.put(code, title); }); } validate(comprobante, asserts) { this._asserts = asserts; this._comprobante = comprobante; this.registerAsserts(); this._sumasConceptos = new cfdiutilsElements.SumasConceptos(comprobante); this.validateSubTotal(); this.validateDescuento(); this.validateTotal(); this.validateImpuestosTrasladados(); this.validateTrasladosMatch(); this.validateImpuestosRetenidos(); this.validateRetencionesMatch(); this.validateDescuentoLessOrEqualThanSubTotal(); return Promise.resolve(); } validateSubTotal() { this.validateValues('SUMAS01', 'Calculado', this._sumasConceptos.getSubTotal(), 'Comprobante', parseFloat(this._comprobante.get('SubTotal') || '0')); } validateDescuento() { this.validateValues('SUMAS02', 'Calculado', this._sumasConceptos.getDescuento(), 'Comprobante', parseFloat(this._comprobante.get('Descuento') || '0')); } validateDescuentoLessOrEqualThanSubTotal() { const subtotal = parseFloat(this._comprobante.get('SubTotal') || '0'); const descuento = parseFloat(this._comprobante.get('Descuento') || '0'); this._asserts.putStatus('SUMAS12', Status.when(subtotal >= descuento), `Subtotal: ${this._comprobante.get('SubTotal')}, Descuento: ${this._comprobante.get('Descuento')}`); } validateTotal() { this.validateValues('SUMAS03', 'Calculado', this._sumasConceptos.getTotal(), 'Comprobante', parseFloat(this._comprobante.get('Total') || '0')); } validateImpuestosTrasladados() { this.validateValues('SUMAS04', 'Calculado', this._sumasConceptos.getImpuestosTrasladados(), 'Comprobante', parseFloat(this._comprobante.searchAttribute('cfdi:Impuestos', 'TotalImpuestosTrasladados') || '0')); } validateImpuestosRetenidos() { this.validateValues('SUMAS08', 'Calculado', this._sumasConceptos.getImpuestosRetenidos(), 'Comprobante', parseFloat(this._comprobante.searchAttribute('cfdi:Impuestos', 'TotalImpuestosRetenidos') || '0')); } validateTrasladosMatch() { this.validateImpuestosMatch(5, 'traslado', this._sumasConceptos.getTraslados(), ['cfdi:Impuestos', 'cfdi:Traslados', 'cfdi:Traslado'], ['Impuesto', 'TipoFactor', 'TasaOCuota']); } validateRetencionesMatch() { this.validateImpuestosMatch(9, 'retención', this._sumasConceptos.getRetenciones(), ['cfdi:Impuestos', 'cfdi:Retenciones', 'cfdi:Retencion'], ['Impuesto']); } validateImpuestosMatch(assertOffset, type, expectedItems, impuestosPath, impuestosKeys) { const extractedItems = {}; this._comprobante.searchNodes(...impuestosPath).forEach(extracted => { const newTemp = {}; impuestosKeys.forEach(impuestoKey => { newTemp[impuestoKey] = extracted.get(impuestoKey); }); newTemp['Importe'] = extracted.get('Importe'); newTemp['Encontrado'] = false; const newKey = cfdiutilsElements.SumasConceptos.impuestoKey(extracted.get('Impuesto'), extracted.get('TipoFactor'), extracted.get('TasaOCuota')); extractedItems[newKey] = newTemp; }); // check that all elements are found and mark extracted item as found let allExpectedAreFound = true; let allValuesMatch = true; let expectedOffSet = 0; Object.entries(expectedItems).forEach(([expectedKey, expectedItem]) => { expectedOffSet = expectedOffSet + 1; let extractedItem; if (!extractedItems[expectedKey]) { allExpectedAreFound = false; extractedItem = { Importe: '' }; } else { // set found flag extractedItems[expectedKey]['Encontrado'] = true; // check value match extractedItem = extractedItems[expectedKey]; } const code = `SUMAS${(assertOffset + 1).toString().padStart(2, '0')}:${expectedOffSet.toString().padStart(3, '0')}`; const thisValueMatch = this.validateImpuestoImporte(type, code, expectedItem, extractedItem); allValuesMatch = allValuesMatch && thisValueMatch; }); const extractedWithoutMatch = Object.values(extractedItems).reduce((a, b) => a + (b['Encontrado'] ? 0 : 1), 0); this._asserts.putStatus(`SUMAS${assertOffset.toString().padStart(2, '0')}`, Status.when(allExpectedAreFound)); this._asserts.putStatus(`SUMAS${(assertOffset + 1).toString().padStart(2, '0')}`, Status.when(allValuesMatch)); this._asserts.putStatus(`SUMAS${(assertOffset + 2).toString().padStart(2, '0')}`, Status.when(0 === extractedWithoutMatch), `No encontrados: ${extractedWithoutMatch}`); } validateImpuestoImporte(type, code, expected, extracted) { let label = `Grupo ${type} Impuesto ${extracted['Impuesto']}`; if (expected['TipoFactor']) { label = `${label} Tipo factor ${expected['TipoFactor']}`; } if (expected['TasaOCuota']) { label = `${label} Tasa o cuota ${expected['TasaOCuota']}`; } this._asserts.put(code, `El importe del impuesto ${label} es igual a el importe del nodo`); return this.validateValues(code, 'Calculado', parseFloat(`${expected['Importe']}`), 'Encontrado', parseFloat(`${extracted['Importe']}`)); } validateValues(code, expectedLabel, expectedValue, compareLabel, compareValue, errorStatus = null) { const condition = this.compareImportesAreEqual(expectedValue, compareValue); this._asserts.putStatus(code, Status.when(condition, errorStatus), `${expectedLabel}: ${expectedValue}, ${compareLabel}: ${compareValue}`); return condition; } compareImportesAreEqual(first, second, delta = null) { if (null === delta) { delta = 0.000001; } return Math.abs(first - second) <= delta; } } class MultiValidatorFactory { newCreated33() { const multiValidator = new MultiValidator('3.3'); multiValidator.add(new XmlFollowSchema()); const standardFiles = [new ComprobanteDecimalesMoneda(), new ComprobanteDescuento(), new ComprobanteFormaPago(), new ComprobanteImpuestos(), new ComprobanteTipoCambio(), new ComprobanteTipoDeComprobante(), new ComprobanteTotal(), new ConceptoDescuento(), new ConceptoImpuestos(), new EmisorRegimenFiscal(), new EmisorRfc(), new FechaComprobante(), new ReceptorResidenciaFiscal(), new ReceptorRfc(), new SelloDigitalCertificado$1(), new SumasConceptosComprobanteImpuestos(), new TimbreFiscalDigitalSello$1(), new TimbreFiscalDigitalVersion$1()]; const recepcionFiles = [new CfdiRelacionados(), new ComplementoPagos(), new ComprobantePagos(), new Conceptos(), new Pago(), new Pagos(), new UsoCfdi()]; multiValidator.addMulti(...standardFiles); multiValidator.addMulti(...recepcionFiles); return multiValidator; } newReceived33() { return this.newCreated33(); } newCreated40() { const multiValidator = new MultiValidator('4.0'); multiValidator.add(new XmlFollowSchema()); multiValidator.add(new XmlDefinition()); const standardFiles = [new SelloDigitalCertificado(), new TimbreFiscalDigitalSello(), new TimbreFiscalDigitalVersion()]; multiValidator.addMulti(...standardFiles); return multiValidator; } newReceived40() { return this.newCreated40(); } } class CfdiValidator33 extends CfdiValidatorTrait { /** * This class uses a default XmlResolver if not provided or null. * If you really want to remove the XmlResolver then use the method setXmlResolver after construction. * * @param xmlResolver - * @param xsltBuilder - */ constructor(xmlResolver = null, xsltBuilder = null) { super(); this.setXmlResolver(xmlResolver || new cfdiutilsCore.XmlResolver()); this.setXsltBuilder(xsltBuilder || new cfdiutilsCore.SaxonbCliBuilder('/usr/bin/saxonb-xslt')); } createVersionedMultiValidator() { const factory = new MultiValidatorFactory(); return factory.newReceived33(); } } class CfdiValidator40 extends CfdiValidatorTrait { /** * This class uses a default XmlResolver if not provided or null. * If you really want to remove the XmlResolver then use the method setXmlResolver after construction. * * @param xmlResolver - * @param xsltBuilder - */ constructor(xmlResolver = null, xsltBuilder = null) { super(); this.setXmlResolver(xmlResolver || new cfdiutilsCore.XmlResolver()); this.setXsltBuilder(xsltBuilder || new cfdiutilsCore.SaxonbCliBuilder('/usr/bin/saxonb-xslt')); } createVersionedMultiValidator() { const factory = new MultiValidatorFactory(); return factory.newReceived40(); } } exports.AbstractDiscoverableVersion33 = AbstractDiscoverableVersion33; exports.AbstractDiscoverableVersion40 = AbstractDiscoverableVersion40; exports.AbstractRecepcionPagos10 = AbstractRecepcionPagos10; exports.AbstractVersion33 = AbstractVersion33; exports.AbstractVersion40 = AbstractVersion40; exports.Assert = Assert; exports.AssertFechaFormat = AssertFechaFormat; exports.Asserts = Asserts; exports.CfdiRelacionados = CfdiRelacionados; exports.CfdiValidator33 = CfdiValidator33; exports.CfdiValidator40 = CfdiValidator40; exports.CfdiValidatorTrait = CfdiValidatorTrait; exports.ComplementoPagos = ComplementoPagos; exports.ComprobanteDecimalesMoneda = ComprobanteDecimalesMoneda; exports.ComprobanteDescuento = ComprobanteDescuento; exports.ComprobanteFormaPago = ComprobanteFormaPago; exports.ComprobanteImpuestos = ComprobanteImpuestos; exports.ComprobantePagos = ComprobantePagos; exports.ComprobanteTipoCambio = ComprobanteTipoCambio; exports.ComprobanteTipoDeComprobante = ComprobanteTipoDeComprobante; exports.ComprobanteTotal = ComprobanteTotal; exports.ConceptoDescuento = ConceptoDescuento; exports.ConceptoImpuestos = ConceptoImpuestos; exports.Conceptos = Conceptos; exports.EmisorRegimenFiscal = EmisorRegimenFiscal; exports.EmisorRfc = EmisorRfc; exports.FechaComprobante = FechaComprobante; exports.Hydrater = Hydrater; exports.MultiValidator = MultiValidator; exports.MultiValidatorFactory = MultiValidatorFactory; exports.Pago = Pago; exports.Pagos = Pagos; exports.ReceptorResidenciaFiscal = ReceptorResidenciaFiscal; exports.ReceptorRfc = ReceptorRfc; exports.SelloDigitalCertificado = SelloDigitalCertificado$1; exports.SelloDigitalCertificado40 = selloDigitalCertificado; exports.SelloDigitalCertificadoValidatorTrait = SelloDigitalCertificadoValidatorTrait; exports.Status = Status; exports.TimbreFiscalDigitalSello = TimbreFiscalDigitalSello$1; exports.TimbreFiscalDigitalSello40 = timbreFiscalDigitalSello; exports.TimbreFiscalDigitalSelloValidatorTrait = TimbreFiscalDigitalSelloValidatorTrait; exports.TimbreFiscalDigitalVersion = TimbreFiscalDigitalVersion$1; exports.TimbreFiscalDigitalVersion40 = timbreFiscalDigitalVersion; exports.TimbreFiscalDigitalVersionValidatorTrait = TimbreFiscalDigitalVersionValidatorTrait; exports.UsoCfdi = UsoCfdi; exports.XmlFollowSchema = XmlFollowSchema; exports.XmlStringPropertyTrait = XmlStringPropertyTrait; //# sourceMappingURL=cfdi-validator.cjs.map