UNPKG

zatca-phase2

Version:

ZATCA Phase 2 e-invoicing integration for Node.js

237 lines (223 loc) 7.38 kB
/** * Invoice XML Generation Module * @module zatca-phase2/xml/invoice * @private */ const js2xmlparser = require('js2xmlparser'); const { v4: uuidv4 } = require('uuid'); const crypto = require('crypto'); const { ZatcaError, ErrorCodes } = require('../errors'); const logger = require('../utils/logger'); const { formatDate, formatTime } = require('../utils/date'); const { validateInvoice } = require('../utils/validation'); /** * Generate XML for an invoice * @function * @param {Object} invoice - Invoice object * @returns {string} XML string */ exports.generateInvoiceXml = function(invoice) { try { logger.debug('Generating invoice XML', { invoiceNumber: invoice.invoiceNumber }); // Validate invoice validateInvoice(invoice); const now = new Date(); const uuid = invoice.uuid || uuidv4(); // Ensure dates are proper Date objects const issueDate = invoice.issueDate instanceof Date ? invoice.issueDate : new Date(invoice.issueDate || now); // Format dates const issueDateFormatted = formatDate(issueDate); const issueTimeFormatted = formatTime(issueDate); // Create invoice XML structure const invoiceObject = { '@': { 'xmlns': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', 'xmlns:cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', 'xmlns:cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', 'xmlns:ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2' }, 'cbc:UBLVersionID': '2.1', 'cbc:ProfileID': invoice.totalAmount >= 1000 ? 'reporting:1.0' : 'standard:reporting:1.0', 'cbc:ID': invoice.invoiceNumber, 'cbc:UUID': uuid, 'cbc:IssueDate': issueDateFormatted, 'cbc:IssueTime': issueTimeFormatted, 'cbc:InvoiceTypeCode': '388', 'cbc:DocumentCurrencyCode': 'SAR', 'cbc:TaxCurrencyCode': 'SAR', 'cac:AdditionalDocumentReference': { 'cbc:ID': 'ICV', 'cbc:UUID': generateICV() }, 'cac:AccountingSupplierParty': { 'cac:Party': { 'cac:PartyIdentification': { 'cbc:ID': invoice.supplierTaxNumber }, 'cac:PartyName': { 'cbc:Name': invoice.supplierName }, 'cac:PostalAddress': { 'cbc:StreetName': invoice.supplierStreet || 'Street', 'cbc:BuildingNumber': invoice.supplierBuilding || '1234', 'cbc:CityName': invoice.supplierCity || 'City', 'cbc:PostalZone': invoice.supplierPostalCode || '12345', 'cbc:CountrySubentity': invoice.supplierRegion || 'Region', 'cac:Country': { 'cbc:IdentificationCode': 'SA' } }, 'cac:PartyTaxScheme': { 'cbc:CompanyID': invoice.supplierTaxNumber, 'cac:TaxScheme': { 'cbc:ID': 'VAT' } } } }, 'cac:AccountingCustomerParty': { 'cac:Party': { 'cac:PartyIdentification': { 'cbc:ID': invoice.customerTaxNumber || 'NA' }, 'cac:PartyName': { 'cbc:Name': invoice.customerName }, 'cac:PostalAddress': { 'cbc:StreetName': invoice.customerStreet || 'Street', 'cbc:BuildingNumber': invoice.customerBuilding || '1234', 'cbc:CityName': invoice.customerCity || 'City', 'cbc:PostalZone': invoice.customerPostalCode || '12345', 'cbc:CountrySubentity': invoice.customerRegion || 'Region', 'cac:Country': { 'cbc:IdentificationCode': 'SA' } }, 'cac:PartyTaxScheme': { 'cbc:CompanyID': invoice.customerTaxNumber || 'NA', 'cac:TaxScheme': { 'cbc:ID': 'VAT' } } } }, 'cac:TaxTotal': { 'cbc:TaxAmount': { '@': { 'currencyID': 'SAR' }, '#': invoice.vatAmount.toFixed(2) }, 'cac:TaxSubtotal': { 'cbc:TaxableAmount': { '@': { 'currencyID': 'SAR' }, '#': (invoice.totalAmount - invoice.vatAmount).toFixed(2) }, 'cbc:TaxAmount': { '@': { 'currencyID': 'SAR' }, '#': invoice.vatAmount.toFixed(2) }, 'cac:TaxCategory': { 'cbc:ID': 'S', 'cbc:Percent': '15.00', 'cac:TaxScheme': { 'cbc:ID': 'VAT' } } } }, 'cac:LegalMonetaryTotal': { 'cbc:LineExtensionAmount': { '@': { 'currencyID': 'SAR' }, '#': (invoice.totalAmount - invoice.vatAmount).toFixed(2) }, 'cbc:TaxExclusiveAmount': { '@': { 'currencyID': 'SAR' }, '#': (invoice.totalAmount - invoice.vatAmount).toFixed(2) }, 'cbc:TaxInclusiveAmount': { '@': { 'currencyID': 'SAR' }, '#': invoice.totalAmount.toFixed(2) }, 'cbc:PayableAmount': { '@': { 'currencyID': 'SAR' }, '#': invoice.totalAmount.toFixed(2) } }, 'cac:InvoiceLine': invoice.items.map((item, index) => ({ 'cbc:ID': (index + 1).toString(), 'cbc:InvoicedQuantity': { '@': { 'unitCode': item.unitCode || 'EA' }, '#': item.quantity.toString() }, 'cbc:LineExtensionAmount': { '@': { 'currencyID': 'SAR' }, '#': (item.quantity * item.unitPrice).toFixed(2) }, 'cac:TaxTotal': { 'cbc:TaxAmount': { '@': { 'currencyID': 'SAR' }, '#': item.taxAmount.toFixed(2) }, 'cbc:RoundingAmount': { '@': { 'currencyID': 'SAR' }, '#': (item.quantity * item.unitPrice + item.taxAmount).toFixed(2) } }, 'cac:Item': { 'cbc:Name': item.name, 'cac:ClassifiedTaxCategory': { 'cbc:ID': 'S', 'cbc:Percent': item.taxRate.toFixed(2), 'cac:TaxScheme': { 'cbc:ID': 'VAT' } } }, 'cac:Price': { 'cbc:PriceAmount': { '@': { 'currencyID': 'SAR' }, '#': item.unitPrice.toFixed(2) } } })) }; // Generate XML const xml = js2xmlparser.parse('Invoice', invoiceObject, { declaration: { include: true, encoding: 'UTF-8' }, format: { doubleQuotes: true } }); logger.debug('Invoice XML generated successfully'); return xml; } catch (error) { logger.error('Failed to generate invoice XML', { error: error.message }); if (error.name === 'ZatcaError') { throw error; } throw new ZatcaError( `Failed to generate invoice XML: ${error.message}`, ErrorCodes.XML_GENERATION_ERROR ); } }; /** * Calculate hash for an invoice XML * @function * @param {string} xml - XML string * @returns {string} SHA-256 hash */ exports.calculateInvoiceHash = function(xml) { return crypto.createHash('sha256').update(xml).digest('hex'); }; /** * Generate Invoice Counter Value (ICV) * @function * @returns {string} Random ICV * @private */ function generateICV() { return crypto.randomBytes(4).toString('hex'); }