n8n-nodes-lhdn-einvoice
Version:
n8n node untuk integrasi dengan LHDN e-Invoice API Malaysia
388 lines (387 loc) • 13.6 kB
JavaScript
;
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';
}