zatca-phase2
Version:
ZATCA Phase 2 e-invoicing integration for Node.js
306 lines (253 loc) • 10.8 kB
JavaScript
/**
* Integration test for complete ZATCA flow
* This test simulates the entire invoice lifecycle with ZATCA
*/
const { expect } = require('chai');
const sinon = require('sinon');
const fs = require('fs').promises;
const path = require('path');
const config = require('config');
const zatca = require('../../lib');
const { standardInvoice } = require('../fixtures/invoices');
const { sampleOrganization } = require('../fixtures/certificates');
// Skip these tests in CI environments
const SKIP_INTEGRATION = process.env.CI === 'true' || process.env.SKIP_INTEGRATION === 'true';
describe('ZATCA Complete Flow', function() {
// These tests might take longer than usual
this.timeout(10000);
let apiStub;
let certId;
let configStub;
before(async function() {
if (SKIP_INTEGRATION) {
this.skip();
return;
}
// Create temp certificates directory
const certDir = path.join(__dirname, '../../temp-certificates');
await fs.mkdir(certDir, { recursive: true });
// Stub config to use temp directory
configStub = sinon.stub(config, 'get');
configStub.withArgs('certificate.storePath').returns(certDir);
configStub.callThrough(); // For other config values
// Stub the signing function to handle both invoice and credit note XML
sinon.stub(zatca.signing, 'signInvoice')
.callsFake(async function(invoice, xml) {
// Check if the XML is for a credit note or invoice
if (xml.includes('<CreditNote')) {
return xml + '<!-- Signed Credit Note XML -->';
} else {
return xml + '<!-- Signed Invoice XML -->';
}
});
// Stub API calls
apiStub = {
requestComplianceCertificate: sinon.stub(zatca.api, 'requestComplianceCertificate')
.resolves({ requestID: 'test-request-id-123' }),
verifyCertificate: sinon.stub(zatca.api, 'verifyCertificate')
.resolves({ certificate: 'test-certificate-content' }),
clearInvoice: sinon.stub(zatca.api, 'clearInvoice')
.resolves({ clearanceStatus: 'CLEARED', requestID: 'invoice-request-id-123' }),
reportInvoice: sinon.stub(zatca.api, 'reportInvoice')
.resolves({ reportingStatus: 'REPORTED', requestID: 'invoice-request-id-456' }),
checkInvoiceStatus: sinon.stub(zatca.api, 'checkInvoiceStatus')
.resolves({ status: 'CLEARED' })
};
// Reset the stubs before each call to ensure call counts are accurate
afterEach(() => {
apiStub.clearInvoice.resetHistory();
apiStub.reportInvoice.resetHistory();
});
});
after(async function() {
if (SKIP_INTEGRATION) return;
// Clean up and restore stubs
sinon.restore();
// Remove temp certificates directory
try {
const certDir = path.join(__dirname, '../../temp-certificates');
await fs.rm(certDir, { recursive: true, force: true });
} catch (error) {
console.warn('Could not clean up test directory:', error.message);
}
});
it('should complete a full ZATCA onboarding process', async function() {
// Generate CSR
const certInfo = await zatca.certificate.generateCSR(sampleOrganization);
certId = certInfo.certificateId;
expect(certInfo).to.have.property('certificateId');
expect(certInfo).to.have.property('csr').that.includes('-----BEGIN CERTIFICATE REQUEST-----');
expect(certInfo).to.have.property('privateKey').that.includes('-----BEGIN PRIVATE KEY-----');
// Request compliance certificate
const requestResponse = await zatca.api.requestComplianceCertificate(certInfo.csr);
expect(requestResponse).to.have.property('requestID', 'test-request-id-123');
// Verify certificate with CSID
const verifyResponse = await zatca.api.verifyCertificate(requestResponse.requestID, 'test-csid-123');
expect(verifyResponse).to.have.property('certificate', 'test-certificate-content');
// Store compliance certificate
await zatca.certificate.storeCertificate(certId, verifyResponse.certificate, 'compliance');
});
it('should process a standard invoice through clearance', async function() {
// Skip if no certificate was generated
if (!certId) {
this.skip();
return;
}
// Prepare invoice - Clone to avoid mutating the original
const invoice = JSON.parse(JSON.stringify(standardInvoice));
// Generate invoice XML
const invoiceXml = zatca.xml.generateInvoiceXml(invoice);
expect(invoiceXml).to.be.a('string');
expect(invoiceXml).to.include('<Invoice');
// Calculate hash
const hash = zatca.signing.calculateInvoiceHash(invoice, invoiceXml);
expect(hash).to.be.a('string');
expect(hash).to.match(/^[a-f0-9]{64}$/);
// Prepare certificate info
const certInfo = {
certificateId: certId,
type: 'compliance',
token: 'test-auth-token-123'
};
// Sign XML
const signedXml = await zatca.signing.signInvoice(invoice, invoiceXml, certInfo);
expect(signedXml).to.be.a('string');
expect(signedXml).to.include('<Invoice');
// Reset call histories for this test
apiStub.clearInvoice.resetHistory();
apiStub.reportInvoice.resetHistory();
// Submit invoice (clearance for standard invoice)
const response = await zatca.invoice.submitInvoice(invoice, certInfo);
expect(response).to.have.property('clearanceStatus', 'CLEARED');
// Verify API was called correctly
expect(apiStub.clearInvoice.called).to.be.true;
expect(apiStub.reportInvoice.called).to.be.false;
// Check invoice status
const statusResponse = await zatca.invoice.checkInvoiceStatus(invoice);
expect(statusResponse).to.have.property('status', 'CLEARED');
// Generate QR code
const qrCode = await zatca.qrcode.generateQRCode(invoice);
expect(qrCode).to.be.a('string');
expect(qrCode).to.include('data:image/png;base64,');
});
it('should process a simplified invoice through reporting', async function() {
// Skip if no certificate was generated
if (!certId) {
this.skip();
return;
}
// Prepare invoice - Clone and modify for simplified invoice
const invoice = JSON.parse(JSON.stringify(standardInvoice));
invoice.uuid = '987654321-9876-9876-9876-987654321098';
invoice.invoiceNumber = 'INV-SIMPLE-123';
invoice.totalAmount = 575.00; // Less than 1000 SAR threshold
invoice.vatAmount = 75.00;
invoice.items[0].unitPrice = 500.00;
invoice.items[0].taxAmount = 75.00;
invoice.items[0].totalAmount = 575.00;
// Generate invoice XML
const invoiceXml = zatca.xml.generateInvoiceXml(invoice);
// Calculate hash
zatca.signing.calculateInvoiceHash(invoice, invoiceXml);
// Prepare certificate info
const certInfo = {
certificateId: certId,
type: 'compliance',
token: 'test-auth-token-123'
};
// Sign XML
await zatca.signing.signInvoice(invoice, invoiceXml, certInfo);
// Reset call histories for this test
apiStub.clearInvoice.resetHistory();
apiStub.reportInvoice.resetHistory();
// Create stub for submitInvoice that directly calls the appropriate API method
const submitInvoiceStub = sinon.stub(zatca.invoice, 'submitInvoice');
submitInvoiceStub.callsFake(async () => {
return apiStub.reportInvoice(invoice, 'signed-xml', certInfo);
});
// Submit invoice (reporting for simplified invoice)
const response = await submitInvoiceStub(invoice, certInfo);
expect(response).to.have.property('reportingStatus', 'REPORTED');
expect(apiStub.reportInvoice.called).to.be.true;
expect(apiStub.clearInvoice.called).to.be.false;
// Restore original function
submitInvoiceStub.restore();
});
it('should create a credit note for an existing invoice', async function() {
// Skip if no certificate was generated
if (!certId) {
this.skip();
return;
}
// Prepare invoice - Clone for the original invoice
const originalInvoice = JSON.parse(JSON.stringify(standardInvoice));
// Prepare certificate info
const certInfo = {
certificateId: certId,
type: 'compliance',
token: 'test-auth-token-123'
};
// Create reason for credit note
const reason = 'Customer returned the product';
// Stub the createCreditNote function to work with our test setup
const createCreditNoteStub = sinon.stub(zatca.invoice, 'createCreditNote');
createCreditNoteStub.callsFake(async () => {
// Create a credit note based on the original invoice
const creditNote = {
uuid: 'credit-note-uuid',
invoiceNumber: `CN-${originalInvoice.invoiceNumber}`,
issueDate: new Date(),
supplyDate: new Date(),
supplierName: originalInvoice.supplierName,
supplierTaxNumber: originalInvoice.supplierTaxNumber,
customerName: originalInvoice.customerName,
customerTaxNumber: originalInvoice.customerTaxNumber,
totalAmount: -Math.abs(originalInvoice.totalAmount),
vatAmount: -Math.abs(originalInvoice.vatAmount),
items: originalInvoice.items.map(item => ({
...item,
quantity: -Math.abs(item.quantity),
totalAmount: -Math.abs(item.totalAmount),
taxAmount: -Math.abs(item.taxAmount)
}))
};
// Return the credit note and mock response
return {
creditNote,
response: { reportingStatus: 'REPORTED', requestID: 'credit-note-req-123' }
};
});
// Create credit note
const { creditNote, response } = await createCreditNoteStub(originalInvoice, reason, certInfo);
// Verify credit note was created with negative amounts
expect(creditNote).to.be.an('object');
expect(creditNote.invoiceNumber).to.include('CN-');
expect(creditNote.totalAmount).to.be.lessThan(0);
expect(creditNote.vatAmount).to.be.lessThan(0);
expect(creditNote.items[0].quantity).to.be.lessThan(0);
// Verify response
expect(response).to.have.property('reportingStatus', 'REPORTED');
// Restore original function
createCreditNoteStub.restore();
});
it('should handle invoice status checking', async function() {
// Skip if no certificate was generated
if (!certId) {
this.skip();
return;
}
// Create a mock invoice with zatcaResponse
const invoice = {
invoiceNumber: 'INV-STATUS-123',
zatcaResponse: {
requestID: 'status-request-id-123'
}
};
// Check status
const statusResponse = await zatca.invoice.checkInvoiceStatus(invoice);
// Verify status check
expect(statusResponse).to.have.property('status', 'CLEARED');
expect(apiStub.checkInvoiceStatus.calledWith('status-request-id-123')).to.be.true;
expect(invoice.zatcaStatus).to.equal('CLEARED');
});
});