6. Integraciones Técnicas
6. Integraciones Técnicas
Esta sección cubre las integraciones técnicas del sistema DTEM con sistemas externos, incluyendo conectores ERP, webhooks, APIs externas y middleware.
6.1. Arquitectura de Integración
6.1.1. Patrones de Integración
Integration Architecture
┌─────────────────────────────────────────────────────────────┐
│ INTEGRATION LAYER │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ERP │ │ CRM │ │ Accounting│ │
│ │ Connectors │ │ Connectors │ │ Connectors │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ API GATEWAY │ │
│ │ ┌─────────────┬─────────────┬─────────────┐ │ │
│ │ │ Auth │ Rate Limit │ Router │ │ │
│ │ └─────────────┴─────────────┴─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MESSAGE QUEUE │ │
│ │ ┌─────────────┬─────────────┬─────────────┐ │ │
│ │ │ Events │ Webhooks │ Tasks │ │ │
│ │ └─────────────┴─────────────┴─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Integration Patterns
| Pattern | Description | Use Case |
|---|---|---|
| API Gateway | Single entry point for all integrations | External system access |
| Event-Driven | Asynchronous communication via events | Real-time notifications |
| Request-Response | Synchronous API calls | Direct data queries |
| Batch Processing | Scheduled bulk data transfers | Historical data sync |
| Webhook | Push notifications to external systems | Status updates |
6.2. Conectores ERP
6.2.1. SAP Connector
SAP Integration Architecture
// SAP Connector Implementation
class SAPConnector {
constructor(config) {
this.config = {
host: config.sapHost,
client: config.sapClient,
user: config.sapUser,
password: config.sapPassword,
lang: config.language || 'ES'
};
}
async connect() {
try {
const sap = require('sapnwrfc');
this.client = await sap.createClient(this.config);
console.log('Connected to SAP system');
return true;
} catch (error) {
console.error('SAP connection failed:', error);
throw error;
}
}
async getCustomerData(rut) {
try {
const result = await this.client.call('BAPI_CUSTOMER_GETDETAIL', {
CUSTOMERNO: rut,
PI_ADDRESS: 'X'
});
return {
rut: result.CUSTOMERNO,
name: result.CUSTOMERNAME,
address: {
street: result.ADDRESS.STREET,
city: result.ADDRESS.CITY,
region: result.ADDRESS.REGION
},
taxInfo: {
ivaRate: result.TAX_INFO.IVA_RATE,
activityCode: result.TAX_INFO.ACTIVITY_CODE
}
};
} catch (error) {
console.error('Error getting customer data:', error);
throw error;
}
}
async createInvoice(dteData) {
try {
const invoiceData = {
HEADER: {
DOC_TYPE: dteData.documentType,
DOC_DATE: dteData.emissionDate,
COMP_CODE: dteData.companyCode,
CURRENCY: 'CLP'
},
CUSTOMER: dteData.customerRut,
ITEMS: dteData.items.map(item => ({
MATERIAL: item.sku,
QUANTITY: item.quantity,
AMOUNT: item.unitPrice
}))
};
const result = await this.client.call('BAPI_INVOICE_CREATE', invoiceData);
if (result.RETURN.TYPE === 'E') {
throw new Error(result.RETURN.MESSAGE);
}
return {
sapDocumentId: result.INVOICE_NO,
status: 'created'
};
} catch (error) {
console.error('Error creating invoice in SAP:', error);
throw error;
}
}
async updateDocumentStatus(dteId, status) {
try {
await this.client.call('Z_DTE_STATUS_UPDATE', {
DTE_ID: dteId,
STATUS: status,
TIMESTAMP: new Date().toISOString()
});
return { success: true };
} catch (error) {
console.error('Error updating document status:', error);
throw error;
}
}
}
SAP Configuration
# sap-connector-config.yaml
sap:
host: "sap.empresa.cl"
client: "100"
sysnr: "00"
user: "DTE_USER"
password: "${SAP_PASSWORD}"
lang: "ES"
endpoints:
customer: "BAPI_CUSTOMER_GETDETAIL"
invoice: "BAPI_INVOICE_CREATE"
status: "Z_DTE_STATUS_UPDATE"
mappings:
documentTypes:
"33": "INV" # Factura
"34": "CR" # Nota de Crédito
"39": "DB" # Nota de Débito
"52": "GU" # Guía de Despacho
retry:
maxAttempts: 3
backoffMs: 1000
timeout:
connection: 30000
request: 60000
6.2.2. Oracle NetSuite Connector
NetSuite Integration
// NetSuite Connector
class NetSuiteConnector {
constructor(config) {
this.config = {
accountId: config.accountId,
consumerKey: config.consumerKey,
consumerSecret: config.consumerSecret,
tokenId: config.tokenId,
tokenSecret: config.tokenSecret,
sandbox: config.sandbox || false
};
this.baseURL = `https://${this.config.accountId}.suitetalk.api.netsuite.com`;
}
async authenticate() {
const oauth = require('oauth-1.0a');
const crypto = require('crypto');
this.oauth = oauth({
consumer: {
key: this.config.consumerKey,
secret: this.config.consumerSecret
},
signature_method: 'HMAC-SHA256',
hash_function(base_string, key) {
return crypto.createHmac('sha256', key).update(base_string).digest('base64');
}
});
this.token = {
key: this.config.tokenId,
secret: this.config.tokenSecret
};
}
async createCustomer(customerData) {
try {
const requestData = {
url: `${this.baseURL}/services/rest/record/v1/customer`,
method: 'POST',
body: JSON.stringify({
companyName: customerData.name,
subsidiary: this.config.subsidiaryId,
isPerson: false,
email: customerData.email,
phone: customerData.phone,
addressBook: [{
addressBookAddress: {
country: customerData.country || 'CL',
state: customerData.region,
city: customerData.city,
addr1: customerData.address
}
}],
customFieldList: [{
scriptId: 'custentity_rut',
value: customerData.rut
}]
})
};
const response = await this.makeRequest(requestData);
return response;
} catch (error) {
console.error('Error creating customer in NetSuite:', error);
throw error;
}
}
async createInvoice(dteData) {
try {
const requestData = {
url: `${this.baseURL}/services/rest/record/v1/invoice`,
method: 'POST',
body: JSON.stringify({
entity: dteData.customerId,
subsidiary: this.config.subsidiaryId,
currency: {
id: '1' // CLP
},
tranDate: dteData.emissionDate,
dueDate: dteData.dueDate,
itemList: {
items: dteData.items.map((item, index) => ({
item: {
id: item.itemId
},
quantity: item.quantity,
rate: item.unitPrice,
amount: item.totalAmount
}))
},
customFieldList: [{
scriptId: 'custbody_dte_id',
value: dteData.id
}, {
scriptId: 'custbody_dte_folio',
value: dteData.folio
}, {
scriptId: 'custbody_dte_type',
value: dteData.documentType
}]
})
};
const response = await this.makeRequest(requestData);
return response;
} catch (error) {
console.error('Error creating invoice in NetSuite:', error);
throw error;
}
}
async makeRequest(requestData) {
const token = this.oauth.authorize(requestData, this.token);
const headers = {
'Content-Type': 'application/json',
'Authorization': this.oauth.toHeader(token),
'User-Agent': 'DTEM-Integration/1.0'
};
const response = await fetch(requestData.url, {
method: requestData.method,
headers: headers,
body: requestData.body
});
if (!response.ok) {
throw new Error(`NetSuite API error: ${response.status} ${response.statusText}`);
}
return await response.json();
}
}
6.3. Webhook System
6.3.1. Webhook Architecture
Webhook Processing Flow
sequenceDiagram
participant DTE as DTE System
participant Queue as Message Queue
participant Processor as Webhook Processor
participant External as External System
DTE->>Queue: Publish Event
Queue->>Processor: Consume Event
Processor->>Processor: Transform Data
Processor->>External: HTTP POST
External-->>Processor: Response
Processor->>Queue: Ack/Nack
alt Success
Processor->>DTE: Success Notification
else Failure
Processor->>Queue: Retry
Processor->>DTE: Failure Notification
end
Webhook Implementation
// Webhook Service
class WebhookService {
constructor(queue, httpClient) {
this.queue = queue;
this.httpClient = httpClient;
this.retryConfig = {
maxAttempts: 3,
backoffMs: 1000,
maxBackoffMs: 30000
};
}
async publishEvent(eventType, data, webhookUrls) {
const event = {
id: crypto.randomUUID(),
type: eventType,
data: data,
timestamp: new Date().toISOString(),
retryCount: 0
};
for (const url of webhookUrls) {
await this.queue.send('webhooks', {
...event,
webhookUrl: url
});
}
return event.id;
}
async processWebhook(webhookEvent) {
try {
const payload = this.transformPayload(webhookEvent);
const response = await this.httpClient.post(
webhookEvent.webhookUrl,
payload,
{
headers: {
'Content-Type': 'application/json',
'X-DTEM-Event': webhookEvent.type,
'X-DTEM-Signature': this.generateSignature(payload),
'X-DTEM-Timestamp': webhookEvent.timestamp
},
timeout: 30000
}
);
if (response.status >= 200 && response.status < 300) {
await this.markWebhookSuccess(webhookEvent.id);
return { success: true, status: response.status };
} else {
throw new Error(`Webhook failed with status ${response.status}`);
}
} catch (error) {
await this.handleWebhookFailure(webhookEvent, error);
throw error;
}
}
transformPayload(event) {
const transformers = {
'dte.created': (data) => ({
event: 'dte.created',
dte: {
id: data.id,
type: data.documentType,
folio: data.folio,
amount: data.totalAmount,
customer: data.rutReceptor,
timestamp: data.createdAt
}
}),
'dte.sent': (data) => ({
event: 'dte.sent',
dte: {
id: data.id,
folio: data.folio,
siiTrackId: data.siiTrackId,
sentAt: data.sentAt
}
}),
'dte.approved': (data) => ({
event: 'dte.approved',
dte: {
id: data.id,
folio: data.folio,
approvedAt: data.siiResponseAt
}
})
};
const transformer = transformers[event.type];
return transformer ? transformer(event.data) : event.data;
}
generateSignature(payload) {
const crypto = require('crypto');
const secret = process.env.WEBHOOK_SECRET;
const payloadString = JSON.stringify(payload);
return crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
}
async handleWebhookFailure(event, error) {
event.retryCount++;
if (event.retryCount <= this.retryConfig.maxAttempts) {
const backoffMs = Math.min(
this.retryConfig.backoffMs * Math.pow(2, event.retryCount - 1),
this.retryConfig.maxBackoffMs
);
setTimeout(async () => {
await this.queue.send('webhooks', event);
}, backoffMs);
} else {
await this.markWebhookFailed(event.id, error.message);
}
}
}
6.3.2. Webhook Configuration
Webhook Registration
// Webhook Registration API
app.post('/api/v1/webhooks', authenticate, validateWebhookRegistration, async (req, res) => {
try {
const { url, events, secret, active = true } = req.body;
// Validate URL
const urlValidation = await validateWebhookUrl(url);
if (!urlValidation.valid) {
return res.status(400).json({
error: 'Invalid webhook URL',
details: urlValidation.error
});
}
// Create webhook subscription
const webhook = await db.query(`
INSERT INTO webhook_subscriptions
(user_id, url, events, secret, active, created_at)
VALUES ($1, $2, $3, $4, $5, NOW())
RETURNING *
`, [req.user.id, url, JSON.stringify(events), secret, active]);
// Send test event
await webhookService.publishEvent('webhook.test', {
webhookId: webhook.rows[0].id,
timestamp: new Date().toISOString()
}, [url]);
res.status(201).json({
webhook: webhook.rows[0],
message: 'Webhook registered successfully'
});
} catch (error) {
console.error('Error registering webhook:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Webhook validation
async function validateWebhookUrl(url) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-DTEM-Event': 'webhook.validation'
},
body: JSON.stringify({
test: true,
timestamp: new Date().toISOString()
}),
timeout: 10000
});
return {
valid: response.status >= 200 && response.status < 300,
status: response.status
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
6.4. Middleware y Message Queue
6.4.1. Message Queue Configuration
RabbitMQ Setup
# rabbitmq-config.yaml
rabbitmq:
exchanges:
- name: dtem.events
type: topic
durable: true
- name: dtem.webhooks
type: fanout
durable: true
queues:
- name: dte.created
exchange: dtem.events
routing_key: dte.created
durable: true
- name: dte.sent
exchange: dtem.events
routing_key: dte.sent
durable: true
- name: webhooks.outbound
exchange: dtem.webhooks
durable: true
- name: erp.integration
exchange: dtem.events
routing_key: erp.*
durable: true
bindings:
- queue: erp.integration
exchange: dtem.events
routing_key: dte.*
Message Producer
// Message Producer
class MessageProducer {
constructor(channel) {
this.channel = channel;
}
async publishEvent(eventType, data, options = {}) {
const message = {
id: crypto.randomUUID(),
type: eventType,
data: data,
timestamp: new Date().toISOString(),
version: '1.0'
};
const routingKey = options.routingKey || eventType;
const exchange = options.exchange || 'dtem.events';
const published = this.channel.publish(
exchange,
routingKey,
Buffer.from(JSON.stringify(message)),
{
messageId: message.id,
timestamp: Date.now(),
contentType: 'application/json',
persistent: true,
headers: {
'X-Event-Type': eventType,
'X-Event-Version': message.version
}
}
);
if (published) {
console.log(`Event published: ${eventType} (${message.id})`);
return message.id;
} else {
throw new Error(`Failed to publish event: ${eventType}`);
}
}
async publishBatch(events) {
const results = [];
for (const event of events) {
try {
const messageId = await this.publishEvent(
event.type,
event.data,
event.options
);
results.push({ success: true, messageId, event });
} catch (error) {
results.push({ success: false, error: error.message, event });
}
}
return results;
}
}
Message Consumer
// Message Consumer
class MessageConsumer {
constructor(channel) {
this.channel = channel;
this.handlers = new Map();
}
async subscribe(queueName, handler, options = {}) {
const defaultOptions = {
noAck: false,
exclusive: false,
durable: true
};
const finalOptions = { ...defaultOptions, ...options };
await this.channel.consume(queueName, async (msg) => {
if (!msg) return;
try {
const event = JSON.parse(msg.content.toString());
console.log(`Processing event: ${event.type} (${event.id})`);
await handler(event);
this.channel.ack(msg);
console.log(`Event processed successfully: ${event.type}`);
} catch (error) {
console.error(`Error processing event:`, error);
if (options.retryOnError !== false) {
// Retry with exponential backoff
const retryDelay = this.calculateRetryDelay(msg);
setTimeout(() => {
this.channel.nack(msg, false, false);
}, retryDelay);
} else {
this.channel.nack(msg, false, false);
}
}
}, finalOptions);
console.log(`Subscribed to queue: ${queueName}`);
}
calculateRetryDelay(msg) {
const retryCount = msg.properties.headers['x-retry-count'] || 0;
return Math.min(1000 * Math.pow(2, retryCount), 30000);
}
async setupDeadLetterQueue(queueName) {
const dlqName = `${queueName}.dlq`;
// Create dead letter exchange
await this.channel.assertExchange(`${queueName}.dlq`, 'fanout', {
durable: true
});
// Create dead letter queue
await this.channel.assertQueue(dlqName, {
durable: true,
arguments: {
'x-dead-letter-exchange': queueName,
'x-message-ttl': 86400000 // 24 hours
}
});
// Bind dead letter queue
await this.channel.bindQueue(dlqName, `${queueName}.dlq`, '');
return dlqName;
}
}
6.5. API Externa SII
6.5.1. SII Integration
SII API Client
// SII API Client
class SIIClient {
constructor(config) {
this.config = {
apiUrl: config.apiUrl || 'https://palena.sii.cl',
certPath: config.certPath,
keyPath: config.keyPath,
timeout: config.timeout || 30000
};
this.httpsAgent = new https.Agent({
cert: fs.readFileSync(this.config.certPath),
key: fs.readFileSync(this.config.keyPath),
rejectUnauthorized: true
});
}
async sendDTE(dteXml) {
try {
const formData = new FormData();
formData.append('rutSender', this.extractRutFromCert());
formData.append('dvSender', this.extractDVFromCert());
formData.append('rutCompany', this.extractCompanyRut(dteXml));
formData.append('dvCompany', this.extractCompanyDV(dteXml));
formData.append('archivo', dteXml, 'dte.xml');
const response = await axios.post(
`${this.config.apiUrl}/cgi_dte/UPL/DTEUpload`,
formData,
{
httpsAgent: this.httpsAgent,
timeout: this.config.timeout,
headers: {
'User-Agent': 'DTEM-System/3.0'
}
}
);
return this.parseSIIResponse(response.data);
} catch (error) {
console.error('Error sending DTE to SII:', error);
throw new SIIError('DTE_SEND_FAILED', error.message);
}
}
async getDTEStatus(trackId) {
try {
const response = await axios.get(
`${this.config.apiUrl}/cgi_dte/UPL/getEstado`,
{
params: {
trackId: trackId
},
httpsAgent: this.httpsAgent,
timeout: this.config.timeout
}
);
return this.parseStatusResponse(response.data);
} catch (error) {
console.error('Error getting DTE status:', error);
throw new SIIError('STATUS_CHECK_FAILED', error.message);
}
}
async downloadDTE(trackId, type = 'PDF') {
try {
const response = await axios.get(
`${this.config.apiUrl}/cgi_dte/UPL/getDocumento`,
{
params: {
trackId: trackId,
tipo: type
},
httpsAgent: this.httpsAgent,
timeout: this.config.timeout,
responseType: 'arraybuffer'
}
);
return {
data: response.data,
contentType: response.headers['content-type'],
filename: `dte_${trackId}.${type.toLowerCase()}`
};
} catch (error) {
console.error('Error downloading DTE:', error);
throw new SIIError('DOWNLOAD_FAILED', error.message);
}
}
parseSIIResponse(responseData) {
const parser = new xml2js.Parser();
return new Promise((resolve, reject) => {
parser.parseString(responseData, (err, result) => {
if (err) {
reject(new SIIError('PARSE_ERROR', err.message));
return;
}
const response = result.RECEPCION_DTE;
if (response.HEADER[0].ESTADO[0] === '0') {
resolve({
success: true,
trackId: response.HEADER[0].TRACKID[0],
timestamp: response.HEADER[0].TIMESTAMP[0],
status: 'received'
});
} else {
reject(new SIIError(
'SII_REJECTED',
response.HEADER[0].ERROR[0]
));
}
});
});
}
extractRutFromCert() {
const cert = fs.readFileSync(this.config.certPath);
const subject = new X509Certificate(cert).subject;
// Extract RUT from certificate subject
const rutMatch = subject.match(/CN=([0-9.-]+)/);
return rutMatch ? rutMatch[1].replace(/[.-]/g, '') : '';
}
}
6.5.2. SII Error Handling
Error Classification
// SII Error Handler
class SIIErrorHandler {
static classifyError(error) {
const errorPatterns = {
'CERTIFICATE_EXPIRED': /certificado.*expirado/i,
'CERTIFICATE_REVOKED': /certificado.*revocado/i,
'INVALID_SIGNATURE': /firma.*invalida/i,
'DUPLICATE_DTE': /duplicado/i,
'INVALID_FORMAT': /formato.*invalido/i,
'NETWORK_ERROR': /network|timeout/i,
'SERVICE_UNAVAILABLE': /servicio.*no.*disponible/i,
'RATE_LIMIT': /excedido.*limite/i
};
for (const [errorType, pattern] of Object.entries(errorPatterns)) {
if (pattern.test(error.message)) {
return errorType;
}
}
return 'UNKNOWN_ERROR';
}
static getRetryStrategy(errorType) {
const strategies = {
'NETWORK_ERROR': { retry: true, delay: 5000, maxAttempts: 3 },
'SERVICE_UNAVAILABLE': { retry: true, delay: 30000, maxAttempts: 5 },
'RATE_LIMIT': { retry: true, delay: 60000, maxAttempts: 3 },
'CERTIFICATE_EXPIRED': { retry: false, action: 'renew_certificate' },
'CERTIFICATE_REVOKED': { retry: false, action: 'renew_certificate' },
'INVALID_SIGNATURE': { retry: false, action: 'fix_signature' },
'DUPLICATE_DTE': { retry: false, action: 'check_duplicate' },
'INVALID_FORMAT': { retry: false, action: 'fix_format' },
'UNKNOWN_ERROR': { retry: true, delay: 10000, maxAttempts: 2 }
};
return strategies[errorType] || strategies['UNKNOWN_ERROR'];
}
static async handleError(error, context) {
const errorType = this.classifyError(error);
const strategy = this.getRetryStrategy(errorType);
// Log error
console.error(`SII Error [${errorType}]:`, error.message);
// Store error for analysis
await this.logError(error, errorType, context);
// Send notification if needed
if (strategy.action) {
await this.sendAlert(errorType, strategy.action, context);
}
return {
errorType,
shouldRetry: strategy.retry,
retryDelay: strategy.delay,
maxAttempts: strategy.maxAttempts,
recommendedAction: strategy.action
};
}
}
6.6. Testing de Integración
6.6.1. Integration Test Suite
Test Framework
// Integration Test Suite
describe('DTEM Integration Tests', () => {
let testConfig;
let sapConnector;
let siiClient;
let webhookService;
beforeAll(async () => {
testConfig = await loadTestConfig();
sapConnector = new SAPConnector(testConfig.sap);
siiClient = new SIIClient(testConfig.sii);
webhookService = new WebhookService(testConfig.webhooks);
});
describe('SAP Integration', () => {
test('should connect to SAP system', async () => {
const connected = await sapConnector.connect();
expect(connected).toBe(true);
});
test('should retrieve customer data', async () => {
const customerData = await sapConnector.getCustomerData('12345678-9');
expect(customerData).toHaveProperty('rut');
expect(customerData).toHaveProperty('name');
expect(customerData).toHaveProperty('address');
expect(customerData).toHaveProperty('taxInfo');
});
test('should create invoice in SAP', async () => {
const dteData = {
documentType: '33',
companyCode: '1000',
emissionDate: '2023-12-01',
customerRut: '12345678-9',
items: [
{
sku: 'PROD001',
quantity: 10,
unitPrice: 10000
}
]
};
const result = await sapConnector.createInvoice(dteData);
expect(result).toHaveProperty('sapDocumentId');
expect(result.status).toBe('created');
});
});
describe('SII Integration', () => {
test('should send DTE to SII', async () => {
const dteXml = generateTestDTEXml();
const result = await siiClient.sendDTE(dteXml);
expect(result.success).toBe(true);
expect(result).toHaveProperty('trackId');
expect(result.status).toBe('received');
});
test('should check DTE status', async () => {
const trackId = '1234567890';
const status = await siiClient.getDTEStatus(trackId);
expect(status).toHaveProperty('status');
expect(status).toHaveProperty('timestamp');
});
});
describe('Webhook Integration', () => {
test('should deliver webhook successfully', async () => {
const webhookUrl = testConfig.webhooks.testUrl;
const eventData = {
id: 'test-dte-123',
type: 'dte.created',
data: {
folio: 1,
amount: 10000
}
};
const eventId = await webhookService.publishEvent(
'dte.created',
eventData,
[webhookUrl]
);
expect(eventId).toBeDefined();
// Wait for webhook delivery
await new Promise(resolve => setTimeout(resolve, 2000));
// Verify webhook was received
const received = await verifyWebhookReceived(eventId);
expect(received).toBe(true);
});
});
});
Próxima sección: 7. Monitoreo y Observabilidad