n8n-nodes-lhdn-einvoice
Version:
n8n node untuk integrasi dengan LHDN e-Invoice API Malaysia
816 lines (815 loc) • 35 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LhdnEInvoice = void 0;
const GenericFunctions_1 = require("./GenericFunctions");
class LhdnEInvoice {
constructor() {
this.description = {
displayName: 'LHDN e-Invoice',
name: 'lhdnEInvoice',
icon: 'file:lhdn-einvoice.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Berinteraksi dengan API LHDN e-Invoice Malaysia',
defaults: {
name: 'LHDN e-Invoice',
},
inputs: ["main"],
outputs: ["main"],
credentials: [
{
name: 'lhdnEInvoiceApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Authentication',
value: 'authentication',
},
{
name: 'Document',
value: 'document',
},
{
name: 'Validation',
value: 'validation',
},
],
default: 'document',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['authentication'],
},
},
options: [
{
name: 'Get Token',
value: 'getToken',
description: 'Dapatkan access token',
action: 'Get access token',
},
],
default: 'getToken',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['document'],
},
},
options: [
{
name: 'Submit',
value: 'submit',
description: 'Hantar dokumen e-invoice',
action: 'Submit document',
},
{
name: 'Get',
value: 'get',
description: 'Dapatkan dokumen berdasarkan UUID',
action: 'Get document',
},
{
name: 'Get Recent',
value: 'getRecent',
description: 'Dapatkan dokumen terkini',
action: 'Get recent documents',
},
{
name: 'Cancel',
value: 'cancel',
description: 'Batalkan dokumen',
action: 'Cancel document',
},
{
name: 'Reject',
value: 'reject',
description: 'Tolak dokumen',
action: 'Reject document',
},
{
name: 'Get Status',
value: 'getStatus',
description: 'Dapatkan status dokumen',
action: 'Get document status',
},
],
default: 'submit',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['validation'],
},
},
options: [
{
name: 'Validate TIN',
value: 'validateTin',
description: 'Sahkan TIN (Tax Identification Number)',
action: 'Validate TIN',
},
],
default: 'validateTin',
},
{
displayName: 'Input Mode',
name: 'inputMode',
type: 'options',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
},
},
options: [
{
name: 'Form Fields (Easy)',
value: 'form',
description: 'Isi borang dengan field-field yang mudah',
},
{
name: 'Raw Document (Advanced)',
value: 'raw',
description: 'Masukkan dokumen JSON/XML secara manual',
},
],
default: 'form',
description: 'Pilih cara untuk memasukkan data invoice',
},
{
displayName: 'Invoice Number',
name: 'invoiceNumber',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'Nombor invoice (contoh: INV001)',
},
{
displayName: 'Invoice Type',
name: 'invoiceType',
type: 'options',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
options: [
{
name: 'Invoice',
value: '01',
description: 'Invoice biasa',
},
{
name: 'Credit Note',
value: '02',
description: 'Nota kredit',
},
{
name: 'Debit Note',
value: '03',
description: 'Nota debit',
},
{
name: 'Refund Note',
value: '11',
description: 'Nota bayaran balik',
},
],
default: '01',
description: 'Jenis dokumen invoice',
},
{
displayName: 'Currency',
name: 'currency',
type: 'options',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
options: [
{
name: 'MYR (Malaysian Ringgit)',
value: 'MYR',
},
{
name: 'USD (US Dollar)',
value: 'USD',
},
{
name: 'SGD (Singapore Dollar)',
value: 'SGD',
},
{
name: 'EUR (Euro)',
value: 'EUR',
},
],
default: 'MYR',
description: 'Mata wang dokumen',
},
{
displayName: 'Supplier TIN',
name: 'supplierTin',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'TIN syarikat pengeluar (contoh: C12345678)',
},
{
displayName: 'Supplier Name',
name: 'supplierName',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'Nama syarikat pengeluar',
},
{
displayName: 'Supplier Address',
name: 'supplierAddress',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'Alamat syarikat pengeluar',
},
{
displayName: 'Supplier City',
name: 'supplierCity',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'Bandar syarikat pengeluar',
},
{
displayName: 'Supplier Postal Code',
name: 'supplierPostalCode',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'Poskod syarikat pengeluar',
},
{
displayName: 'Supplier State',
name: 'supplierState',
type: 'options',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
options: [
{ name: 'Johor', value: '01' },
{ name: 'Kedah', value: '02' },
{ name: 'Kelantan', value: '03' },
{ name: 'Melaka', value: '04' },
{ name: 'Negeri Sembilan', value: '05' },
{ name: 'Pahang', value: '06' },
{ name: 'Pulau Pinang', value: '07' },
{ name: 'Perak', value: '08' },
{ name: 'Perlis', value: '09' },
{ name: 'Selangor', value: '10' },
{ name: 'Terengganu', value: '11' },
{ name: 'Sabah', value: '12' },
{ name: 'Sarawak', value: '13' },
{ name: 'Kuala Lumpur', value: '14' },
{ name: 'Labuan', value: '15' },
{ name: 'Putrajaya', value: '16' },
],
default: '14',
required: true,
description: 'Negeri syarikat pengeluar',
},
{
displayName: 'Customer TIN',
name: 'customerTin',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'TIN pelanggan (contoh: D98765432)',
},
{
displayName: 'Customer Name',
name: 'customerName',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
required: true,
description: 'Nama pelanggan',
},
{
displayName: 'Customer Address',
name: 'customerAddress',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
description: 'Alamat pelanggan (optional)',
},
{
displayName: 'Customer City',
name: 'customerCity',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: '',
description: 'Bandar pelanggan (optional)',
},
{
displayName: 'Invoice Items',
name: 'invoiceItems',
type: 'fixedCollection',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['form'],
},
},
default: {},
typeOptions: {
multipleValues: true,
},
description: 'Item-item dalam invoice',
options: [
{
name: 'item',
displayName: 'Item',
values: [
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
required: true,
description: 'Penerangan item',
},
{
displayName: 'Quantity',
name: 'quantity',
type: 'number',
default: 1,
required: true,
description: 'Kuantiti item',
},
{
displayName: 'Unit Price',
name: 'unitPrice',
type: 'number',
default: 0,
required: true,
description: 'Harga per unit',
},
{
displayName: 'Tax Rate (%)',
name: 'taxRate',
type: 'number',
default: 6,
description: 'Kadar cukai (default: 6% SST)',
},
{
displayName: 'Tax Type',
name: 'taxType',
type: 'options',
options: [
{
name: 'Standard Rate (6%)',
value: '01',
},
{
name: 'Zero Rate (0%)',
value: '02',
},
{
name: 'Exempt',
value: '03',
},
],
default: '01',
description: 'Jenis cukai',
},
],
},
],
},
{
displayName: 'Document Format',
name: 'documentFormat',
type: 'options',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['raw'],
},
},
options: [
{
name: 'JSON',
value: 'JSON',
},
{
name: 'XML',
value: 'XML',
},
],
default: 'JSON',
description: 'Format dokumen yang akan dihantar',
},
{
displayName: 'Document',
name: 'document',
type: 'json',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
inputMode: ['raw'],
},
},
default: '',
required: true,
description: 'Dokumen e-invoice dalam format JSON atau XML',
},
{
displayName: 'Code Number',
name: 'codeNumber',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['submit'],
},
},
default: '',
required: true,
description: 'Kod nombor untuk dokumen',
},
{
displayName: 'Document UUID',
name: 'documentUuid',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['get', 'cancel', 'reject', 'getStatus'],
},
},
default: '',
required: true,
description: 'UUID dokumen yang ingin diakses',
},
{
displayName: 'Page Number',
name: 'pageNo',
type: 'number',
displayOptions: {
show: {
resource: ['document'],
operation: ['getRecent'],
},
},
default: 1,
description: 'Nombor halaman untuk pagination',
},
{
displayName: 'Page Size',
name: 'pageSize',
type: 'number',
displayOptions: {
show: {
resource: ['document'],
operation: ['getRecent'],
},
},
default: 20,
description: 'Saiz halaman untuk pagination',
},
{
displayName: 'Submission Date From',
name: 'submissionDateFrom',
type: 'dateTime',
displayOptions: {
show: {
resource: ['document'],
operation: ['getRecent'],
},
},
default: '',
description: 'Tarikh mula untuk carian dokumen',
},
{
displayName: 'Submission Date To',
name: 'submissionDateTo',
type: 'dateTime',
displayOptions: {
show: {
resource: ['document'],
operation: ['getRecent'],
},
},
default: '',
description: 'Tarikh akhir untuk carian dokumen',
},
{
displayName: 'Reason',
name: 'reason',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['cancel', 'reject'],
},
},
default: '',
required: true,
description: 'Sebab untuk pembatalan atau penolakan',
},
{
displayName: 'TIN',
name: 'tin',
type: 'string',
displayOptions: {
show: {
resource: ['validation'],
operation: ['validateTin'],
},
},
default: '',
required: true,
description: 'Tax Identification Number untuk disahkan',
},
{
displayName: 'ID Type',
name: 'idType',
type: 'options',
displayOptions: {
show: {
resource: ['validation'],
operation: ['validateTin'],
},
},
options: [
{
name: 'NRIC',
value: 'NRIC',
},
{
name: 'Passport',
value: 'PASSPORT',
},
{
name: 'BRN',
value: 'BRN',
},
{
name: 'Army',
value: 'ARMY',
},
],
default: 'NRIC',
description: 'Jenis ID untuk pengesahan TIN',
},
{
displayName: 'ID Value',
name: 'idValue',
type: 'string',
displayOptions: {
show: {
resource: ['validation'],
operation: ['validateTin'],
},
},
default: '',
required: true,
description: 'Nilai ID untuk pengesahan TIN',
},
],
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
let responseData;
if (resource === 'authentication') {
if (operation === 'getToken') {
responseData = await GenericFunctions_1.getAccessToken.call(this);
}
}
else if (resource === 'document') {
const accessToken = await GenericFunctions_1.getAccessToken.call(this);
if (operation === 'submit') {
const inputMode = this.getNodeParameter('inputMode', i);
const codeNumber = this.getNodeParameter('codeNumber', i);
let document;
let documentFormat = 'JSON';
if (inputMode === 'form') {
const formData = {
invoiceNumber: this.getNodeParameter('invoiceNumber', i),
invoiceType: this.getNodeParameter('invoiceType', i),
currency: this.getNodeParameter('currency', i),
supplierTin: this.getNodeParameter('supplierTin', i),
supplierName: this.getNodeParameter('supplierName', i),
supplierAddress: this.getNodeParameter('supplierAddress', i),
supplierCity: this.getNodeParameter('supplierCity', i),
supplierPostalCode: this.getNodeParameter('supplierPostalCode', i),
supplierState: this.getNodeParameter('supplierState', i),
customerTin: this.getNodeParameter('customerTin', i),
customerName: this.getNodeParameter('customerName', i),
customerAddress: this.getNodeParameter('customerAddress', i),
customerCity: this.getNodeParameter('customerCity', i),
invoiceItems: this.getNodeParameter('invoiceItems', i),
};
const invoiceDocument = (0, GenericFunctions_1.buildInvoiceFromFormFields)(formData);
document = JSON.stringify(invoiceDocument);
documentFormat = 'JSON';
}
else {
documentFormat = this.getNodeParameter('documentFormat', i);
document = this.getNodeParameter('document', i);
}
const documentBase64 = Buffer.from(document).toString('base64');
const crypto = require('crypto');
const documentHash = crypto.createHash('sha256').update(document).digest('hex');
const body = {
documents: [
{
format: documentFormat,
documentHash: documentHash,
codeNumber: codeNumber,
document: documentBase64,
},
],
};
responseData = await GenericFunctions_1.lhdnEInvoiceApiRequest.call(this, 'POST', '/api/v1.0/documentsubmissions', body, {}, accessToken.access_token);
}
else if (operation === 'get') {
const documentUuid = this.getNodeParameter('documentUuid', i);
responseData = await GenericFunctions_1.lhdnEInvoiceApiRequest.call(this, 'GET', `/api/v1.0/documents/${documentUuid}/raw`, {}, {}, accessToken.access_token);
}
else if (operation === 'getRecent') {
const pageNo = this.getNodeParameter('pageNo', i);
const pageSize = this.getNodeParameter('pageSize', i);
const submissionDateFrom = this.getNodeParameter('submissionDateFrom', i);
const submissionDateTo = this.getNodeParameter('submissionDateTo', i);
const qs = {
PageNo: pageNo,
PageSize: pageSize,
};
if (submissionDateFrom) {
qs.SubmissionDateFrom = submissionDateFrom;
}
if (submissionDateTo) {
qs.SubmissionDateTo = submissionDateTo;
}
responseData = await GenericFunctions_1.lhdnEInvoiceApiRequest.call(this, 'GET', '/api/v1.0/documents/recent', {}, qs, accessToken.access_token);
}
else if (operation === 'cancel') {
const documentUuid = this.getNodeParameter('documentUuid', i);
const reason = this.getNodeParameter('reason', i);
const body = {
status: 'cancelled',
reason: reason,
};
responseData = await GenericFunctions_1.lhdnEInvoiceApiRequest.call(this, 'PUT', `/api/v1.0/documents/state/${documentUuid}/state`, body, {}, accessToken.access_token);
}
else if (operation === 'reject') {
const documentUuid = this.getNodeParameter('documentUuid', i);
const reason = this.getNodeParameter('reason', i);
const body = {
status: 'rejected',
reason: reason,
};
responseData = await GenericFunctions_1.lhdnEInvoiceApiRequest.call(this, 'PUT', `/api/v1.0/documents/state/${documentUuid}/state`, body, {}, accessToken.access_token);
}
else if (operation === 'getStatus') {
const documentUuid = this.getNodeParameter('documentUuid', i);
responseData = await GenericFunctions_1.lhdnEInvoiceApiRequest.call(this, 'GET', `/api/v1.0/documents/${documentUuid}/details`, {}, {}, accessToken.access_token);
}
}
else if (resource === 'validation') {
if (operation === 'validateTin') {
const accessToken = await GenericFunctions_1.getAccessToken.call(this);
const tin = this.getNodeParameter('tin', i);
const idType = this.getNodeParameter('idType', i);
const idValue = this.getNodeParameter('idValue', i);
responseData = await GenericFunctions_1.lhdnEInvoiceApiRequest.call(this, 'GET', `/api/v1.0/taxpayer/validate/${tin}`, {}, {
idType: idType,
idValue: idValue,
}, accessToken.access_token);
}
}
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
returnData.push(...executionData);
}
catch (error) {
if (this.continueOnFail()) {
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } });
returnData.push(...executionData);
continue;
}
throw error;
}
}
return [returnData];
}
}
exports.LhdnEInvoice = LhdnEInvoice;