UNPKG

n8n-nodes-lhdn-einvoice

Version:

n8n node untuk integrasi dengan LHDN e-Invoice API Malaysia

388 lines (387 loc) 13.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.lhdnEInvoiceApiRequest = lhdnEInvoiceApiRequest; exports.getAccessToken = getAccessToken; exports.calculateSHA256Hash = calculateSHA256Hash; exports.convertToBase64 = convertToBase64; exports.generateDigitalSignature = generateDigitalSignature; exports.validateInvoiceDocument = validateInvoiceDocument; exports.buildInvoiceFromFormFields = buildInvoiceFromFormFields; exports.formatDateForAPI = formatDateForAPI; exports.formatTimeForAPI = formatTimeForAPI; exports.generateUUID = generateUUID; exports.parseErrorResponse = parseErrorResponse; const n8n_workflow_1 = require("n8n-workflow"); const crypto = __importStar(require("crypto")); async function lhdnEInvoiceApiRequest(method, resource, body = {}, qs = {}, accessToken) { const credentials = await this.getCredentials('lhdnEInvoiceApi'); const environment = credentials.environment; const baseUrl = environment === 'production' ? 'https://api.myinvois.hasil.gov.my' : 'https://preprod-api.myinvois.hasil.gov.my'; const options = { method: method, body, qs, url: `${baseUrl}${resource}`, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, json: true, }; if (accessToken) { options.headers.Authorization = `Bearer ${accessToken}`; } try { return await this.helpers.httpRequest(options); } catch (error) { throw new n8n_workflow_1.NodeApiError(this.getNode(), error); } } async function getAccessToken() { const credentials = await this.getCredentials('lhdnEInvoiceApi'); const clientId = credentials.clientId; const clientSecret = credentials.clientSecret; const environment = credentials.environment; const baseUrl = environment === 'production' ? 'https://api.myinvois.hasil.gov.my' : 'https://preprod-api.myinvois.hasil.gov.my'; const body = new URLSearchParams(); body.append('client_id', clientId); body.append('client_secret', clientSecret); body.append('grant_type', 'client_credentials'); body.append('scope', 'InvoicingAPI'); const options = { method: 'POST', body: body.toString(), url: `${baseUrl}/connect/token`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, json: true, }; try { return await this.helpers.httpRequest(options); } catch (error) { throw new n8n_workflow_1.NodeApiError(this.getNode(), error); } } function calculateSHA256Hash(data) { return crypto.createHash('sha256').update(data).digest('hex'); } function convertToBase64(data) { return Buffer.from(data).toString('base64'); } function generateDigitalSignature(document, privateKey, certificate) { const signature = { signatureValue: '', keyInfo: { x509Data: { x509Certificate: certificate, }, }, signedInfo: { canonicalizationMethod: 'http://www.w3.org/2001/10/xml-exc-c14n#', signatureMethod: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', reference: { digestMethod: 'http://www.w3.org/2001/04/xmlenc#sha256', digestValue: calculateSHA256Hash(document), }, }, }; return signature; } function validateInvoiceDocument(document, format) { if (!document) { return false; } if (format === 'JSON') { if (typeof document !== 'object' || !document.Invoice) { return false; } const invoice = document.Invoice; const mandatoryFields = [ 'InvoiceTypeCode', 'ID', 'IssueDate', 'IssueTime', 'DocumentCurrencyCode', 'AccountingSupplierParty', 'AccountingCustomerParty', 'InvoiceLine', ]; for (const field of mandatoryFields) { if (!invoice[field]) { return false; } } return true; } else if (format === 'XML') { if (typeof document !== 'string') { return false; } return document.includes('<Invoice') && document.includes('</Invoice>'); } return false; } function buildInvoiceFromFormFields(formData) { const currentDate = new Date(); const issueDate = currentDate.toISOString().split('T')[0]; const issueTime = currentDate.toTimeString().split(' ')[0]; let subtotal = 0; let totalTax = 0; const invoiceLines = []; if (formData.invoiceItems && formData.invoiceItems.item) { formData.invoiceItems.item.forEach((item, index) => { const lineExtensionAmount = item.quantity * item.unitPrice; const taxRate = item.taxRate !== undefined ? item.taxRate : 6; const taxAmount = (lineExtensionAmount * taxRate) / 100; subtotal += lineExtensionAmount; totalTax += taxAmount; invoiceLines.push({ ID: (index + 1).toString(), InvoicedQuantity: { '@unitCode': 'C62', '#text': item.quantity.toString() }, LineExtensionAmount: { '@currencyID': formData.currency || 'MYR', '#text': lineExtensionAmount.toFixed(2) }, Item: { Name: item.description }, Price: { PriceAmount: { '@currencyID': formData.currency || 'MYR', '#text': item.unitPrice.toFixed(2) } }, TaxTotal: { TaxAmount: { '@currencyID': formData.currency || 'MYR', '#text': taxAmount.toFixed(2) }, TaxSubtotal: { TaxableAmount: { '@currencyID': formData.currency || 'MYR', '#text': lineExtensionAmount.toFixed(2) }, TaxAmount: { '@currencyID': formData.currency || 'MYR', '#text': taxAmount.toFixed(2) }, TaxCategory: { ID: item.taxType || '01', TaxScheme: { ID: 'OTH' } } } } }); }); } const totalAmount = subtotal + totalTax; const invoice = { '@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', UBLVersionID: '2.1', CustomizationID: 'MY:UBL:2.1', ProfileID: 'reporting:1.0', ID: formData.invoiceNumber, IssueDate: issueDate, IssueTime: issueTime, InvoiceTypeCode: { '@listVersionID': '1.0', '#text': formData.invoiceType || '01' }, DocumentCurrencyCode: formData.currency || 'MYR', TaxCurrencyCode: formData.currency || 'MYR', InvoicePeriod: { StartDate: issueDate, EndDate: issueDate }, AccountingSupplierParty: { Party: { PartyIdentification: { ID: { '@schemeID': 'TIN', '#text': formData.supplierTin } }, PartyName: { Name: formData.supplierName }, PostalAddress: { AddressLine: { Line: formData.supplierAddress }, CityName: formData.supplierCity, PostalZone: formData.supplierPostalCode, CountrySubentityCode: formData.supplierState, Country: { IdentificationCode: 'MY' } }, PartyTaxScheme: { TaxScheme: { ID: 'OTH' } }, PartyLegalEntity: { RegistrationName: formData.supplierName } } }, AccountingCustomerParty: { Party: { PartyIdentification: { ID: { '@schemeID': 'TIN', '#text': formData.customerTin } }, PartyName: { Name: formData.customerName }, PostalAddress: formData.customerAddress ? { AddressLine: { Line: formData.customerAddress }, CityName: formData.customerCity || '', Country: { IdentificationCode: 'MY' } } : { Country: { IdentificationCode: 'MY' } }, PartyTaxScheme: { TaxScheme: { ID: 'OTH' } }, PartyLegalEntity: { RegistrationName: formData.customerName } } }, PaymentMeans: { PaymentMeansCode: '01', PaymentDueDate: issueDate }, TaxTotal: { TaxAmount: { '@currencyID': formData.currency || 'MYR', '#text': totalTax.toFixed(2) }, TaxSubtotal: { TaxableAmount: { '@currencyID': formData.currency || 'MYR', '#text': subtotal.toFixed(2) }, TaxAmount: { '@currencyID': formData.currency || 'MYR', '#text': totalTax.toFixed(2) }, TaxCategory: { ID: '01', TaxScheme: { ID: 'OTH' } } } }, LegalMonetaryTotal: { LineExtensionAmount: { '@currencyID': formData.currency || 'MYR', '#text': subtotal.toFixed(2) }, TaxExclusiveAmount: { '@currencyID': formData.currency || 'MYR', '#text': subtotal.toFixed(2) }, TaxInclusiveAmount: { '@currencyID': formData.currency || 'MYR', '#text': totalAmount.toFixed(2) }, PayableAmount: { '@currencyID': formData.currency || 'MYR', '#text': totalAmount.toFixed(2) } }, InvoiceLine: invoiceLines }; return { Invoice: invoice }; } function formatDateForAPI(date) { if (isNaN(date.getTime())) { return new Date().toISOString().split('T')[0]; } return date.toISOString().split('T')[0]; } function formatTimeForAPI(date) { if (isNaN(date.getTime())) { return new Date().toISOString().split('T')[1]; } return date.toISOString().split('T')[1]; } function generateUUID() { return crypto.randomUUID(); } function parseErrorResponse(error) { if (error.response && error.response.data) { if (error.response.data.error_description) { return error.response.data.error_description; } if (error.response.data.message) { return error.response.data.message; } if (typeof error.response.data === 'string') { return error.response.data; } } return error.message || 'Unknown error occurred'; }