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