UNPKG

@facturacr/atv-sdk

Version:

Librería (SDK) de Javascript/NodeJS para acceder al API de Administración Tributaria Virtual (ATV) del Ministerio de Hacienda.

202 lines (189 loc) 7.93 kB
import { AtvDocument, InvoiceDocumentContainer, DetalleServicio, Resumen, Persona, InformacionReferencia } from '@src/types/facturaInterfaces' import { Document as DomainDocument } from '../core/Document' import { OrderLine } from '../core/OrderLine' import { Person } from '../core/Person' import { ReferenceInformation } from '../core/ReferenceInformation' import { ReceptorMessageProps } from '../core/types' type AtvFormat = InvoiceDocumentContainer const parseAtvMoneyFormat = (amount: number) => { return parseFloat(amount.toFixed(5)) } const mapOrderLinesToAtvFormat = (orderLines: OrderLine[]): DetalleServicio => { const LineaDetalle = orderLines.map<DetalleServicio['LineaDetalle'][0]>((orderLine) => { const impuestoMonto = orderLine.tax ? parseAtvMoneyFormat(orderLine.tax.amount ?? 0) : 0 return { NumeroLinea: orderLine.lineNumber, CodigoCABYS: orderLine.code, // CodigoComercial Cantidad: orderLine.quantity, UnidadMedida: orderLine.measureUnit, // UnidadMedidaComercial Detalle: orderLine.detail, PrecioUnitario: orderLine.unitaryPrice, MontoTotal: orderLine.totalAmount, // Descuento SubTotal: orderLine.subTotal, BaseImponible: orderLine.subTotal, ...(orderLine.tax && { Impuesto: { Codigo: orderLine.tax.code, CodigoTarifaIVA: orderLine.tax.rateCode, Tarifa: orderLine.tax.rate, Monto: impuestoMonto // Exoneracion is explicitly excluded here } }), ImpuestoAsumidoEmisorFabrica: 0, // @ts-expect-error pending-to-fix ImpuestoNeto: parseAtvMoneyFormat(orderLine.tax.amount), MontoTotalLinea: parseAtvMoneyFormat(orderLine.totalOrderLineAmount), exchangeRate: orderLine.exchangeRate, currency: orderLine.currency } }) return { LineaDetalle } } const mapSummaryInvoice = (document: DomainDocument): Resumen => { const summaryInvoice = document.summaryInvoice const orderLines = document.orderLines // Still need access to order lines for breakdown // --- Lógica para TotalDesgloseImpuesto (sin considerar exoneraciones) --- const taxBreakdownMap = new Map<string, number>() orderLines.forEach(orderLine => { if (orderLine.tax?.amount !== undefined && orderLine.tax.amount !== null && orderLine.tax.amount > 0) { const key = `${orderLine.tax.code}-${orderLine.tax.rateCode}` const currentTotal = taxBreakdownMap.get(key) || 0 taxBreakdownMap.set(key, currentTotal + orderLine.tax.amount) } }) const TotalDesgloseImpuesto = Array.from(taxBreakdownMap.entries()).map(([key, totalMonto]) => { const [Codigo, CodigoTarifaIVA, Tarifa] = key.split('-') return { Codigo, CodigoTarifaIVA, Tarifa, TotalMontoImpuesto: parseAtvMoneyFormat(totalMonto) } }) return { CodigoTipoMoneda: { // @ts-expect-error pending-to-fix CodigoMoneda: summaryInvoice.currency.code, // @ts-expect-error pending-to-fix TipoCambio: summaryInvoice.currency.exchangeRate }, TotalServGravados: parseAtvMoneyFormat(summaryInvoice.totalEncumberedServices), TotalServExentos: parseAtvMoneyFormat(summaryInvoice.totalExemptServices), TotalServNoSujeto: parseAtvMoneyFormat(summaryInvoice.totalNonTaxableServices), // Moved here // @ts-expect-error pending-to-fix TotalMercanciasGravadas: parseAtvMoneyFormat(summaryInvoice.totalEncumberedMerchandise), // @ts-expect-error pending-to-fix TotalMercanciasExentas: parseAtvMoneyFormat(summaryInvoice.totalExemptMerchandise), TotalMercNoSujeta: parseAtvMoneyFormat(summaryInvoice.totalNonTaxableMerchandise), // Moved here TotalGravado: parseAtvMoneyFormat(summaryInvoice.totalEncumbered), TotalExento: parseAtvMoneyFormat(summaryInvoice.totalExempt), TotalExonerado: parseAtvMoneyFormat(summaryInvoice.totalExonerated), TotalNoSujeto: parseAtvMoneyFormat(summaryInvoice.totalNonTaxable), // Moved here TotalVenta: parseAtvMoneyFormat(summaryInvoice.totalSale), // @ts-expect-error pending-to-fix TotalDescuentos: parseAtvMoneyFormat(summaryInvoice.totalDiscounts), // @ts-expect-error pending-to-fix TotalVentaNeta: parseAtvMoneyFormat(summaryInvoice.totalNetSale), TotalDesgloseImpuesto, TotalImpuesto: parseAtvMoneyFormat(summaryInvoice.totalTaxes), TotalImpAsumEmisorFabrica: 0, TotalOtrosCargos: 0, MedioPago: { // @ts-expect-error pending-to-fix TipoMedioPago: document.paymentMethod }, TotalComprobante: parseAtvMoneyFormat(summaryInvoice.totalVoucher) } } const mapPerson = (person: Person): Persona => { const atvPerson = { Nombre: person.fullName, Identificacion: { Tipo: person.identifierType, Numero: person.identifierId }, NombreComercial: person.commercialName, Ubicacion: undefined, Telefono: undefined, CorreoElectronico: undefined } // @ts-expect-error pending-to-fix atvPerson.Ubicacion = person.location ? { Provincia: person.location?.province, Canton: person.location?.canton?.padStart(2, '0'), Distrito: person.location?.district?.padStart(2, '0'), Barrio: person.location?.neighborhood?.padStart(5, '0'), OtrasSenas: person.location?.details } : undefined // @ts-expect-error pending-to-fix atvPerson.Telefono = person.phone ? { CodigoPais: person.phone?.countryCode, NumTelefono: person.phone?.number } : undefined // @ts-expect-error pending-to-fix atvPerson.CorreoElectronico = person.email return atvPerson } const mapReferenceInformation = (referenceInfo: ReferenceInformation): InformacionReferencia => { return { TipoDocIR: referenceInfo.docType, Numero: referenceInfo.refNumber, FechaEmisionIR: referenceInfo.issueDate.toISOString(), Codigo: referenceInfo.code, Razon: referenceInfo.reason } } export const mapDocumentToAtvFormat = (docName: string, document: DomainDocument): AtvFormat => { const key = docName const doc: AtvDocument = { Clave: document.clave, ProveedorSistemas: document.providerId, CodigoActividadEmisor: document.emitter.activityCode.padStart(6, '0'), ...(document.receiver && { // TODO add && document.name === 'FacturaElectronica' CodigoActividadReceptor: document.receiver?.activityCode?.padStart(6, '0') }), NumeroConsecutivo: document.fullConsecutive, FechaEmision: document.issueDate.toISOString(), Emisor: mapPerson(document.emitter), ...(document.receiver && { Receptor: mapPerson(document.receiver) }), CondicionVenta: document.conditionSale, PlazoCredito: document.deadlineCredit, DetalleServicio: mapOrderLinesToAtvFormat(document.orderLines), ResumenFactura: mapSummaryInvoice(document), Otros: document.others } if (document.referenceInformation) { doc.InformacionReferencia = mapReferenceInformation(document.referenceInformation) } return { [key]: doc } } export const mapReceptorMessageToAtvFormat = (props: ReceptorMessageProps): AtvFormat => { return { MensajeReceptor: { Clave: props.clave, NumeroCedulaEmisor: props.emitterIdentifier, FechaEmisionDoc: props.documentIssueDate.toISOString(), Mensaje: props.aceptationState.toString(), // 1 Aceptado | 2 Aceptado Parcialmente | 3 Rechazado DetalleMensaje: props.aceptationDetailMessage, MontoTotalImpuesto: props.totalTaxes, CodigoActividad: props.activityCode, CondicionImpuesto: props.taxCondition, MontoTotalDeGastoAplicable: props.totalSale, // fullInvoice.ResumenFactura.TotalVenta, // TODO investigar casos de uso TotalFactura: props.totalSale, // fullInvoice.ResumenFactura.TotalVenta, NumeroCedulaReceptor: props.receptorIdentifier, NumeroConsecutivoReceptor: props.receptorConcecutive } } }