@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
text/typescript
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
}
}
}