Saltar al contenido principal

RPAfelApi Integration Implementation Guide

🎯 Updated Recommendation: Use RPAfelApi ⭐⭐⭐

After reviewing the complete API documentation, RPAfelApi is the clear winner:

Why Use RPAfelApi Now

  1. ✅ Already Production-Ready

    • Deployed at: https://fel.rpapos.com/api/fel
    • Production-tested
    • Comprehensive error handling
  2. ✅ All Features Included

    • Certificate caching (for Infile)
    • "Already signed" error handling (406)
    • Support for all document types (FACT, NCRE, NDEB, ANULACION)
    • Void certificate endpoint
    • Query payer info
    • Email sending
  3. ✅ Faster Implementation

    • No need to build caching
    • No need to implement error handling
    • Just integrate the API
  4. ✅ Comprehensive Documentation

    • Complete API guide
    • Code examples in multiple languages
    • Clear request/response formats

🏗️ Implementation Plan

Phase 1: Create RPAfelApi Provider Service

File: apps/backend/src/fel/infrastructure/provider-rpafelapi.service.ts

import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom } from 'rxjs';
import type { ElectronicCertificationProvider } from '@/fel/domain/electronic-certification-provider.interface';
import type {
IProviderCertifyDocumentParameters,
IProviderGetSharedInfoParameters,
} from '@/fel/domain/fel.interface';

@Injectable()
export class ProviderRpaFelApiService implements ElectronicCertificationProvider {
constructor(private readonly httpService: HttpService) {}

private readonly baseUrl =
process.env.RPA_FEL_API_URL ||
process.env.NODE_ENV === 'production'
? 'https://fel.rpapos.com/api/fel'
: 'https://fel-dev.rpapos.com/api/fel';

async certifyDocument({
taxId,
xmlContent,
apiUrl,
token,
}: IProviderCertifyDocumentParameters): Promise<undefined> {
// Convert XML to DTE JSON structure
const dteJson = this.convertXmlToDteJson(xmlContent, taxId);

try {
const response = await lastValueFrom(
this.httpService.post(
`${this.baseUrl}/generateCertificateToSign`,
dteJson,
{
headers: {
Authorization: token, // RPAfelApi expects token directly
'Content-Type': 'application/json',
},
timeout: 60000, // 60 seconds (certification can take time)
}
)
);

// Transform RPAfelApi response to match certifier response format
return this.transformResponse(response.data);
} catch (error) {
// Handle errors
if (error.response?.status === 406) {
// Already signed - RPAfelApi handles this automatically
// It will fetch from certifier and return
throw new Error('Document already signed - RPAfelApi will handle');
}
throw error;
}
}

async getSharedInfo({
taxId,
data1,
data2,
username,
apiUrl,
token,
}: IProviderGetSharedInfoParameters): Promise<unknown> {
const response = await lastValueFrom(
this.httpService.post(
`${this.baseUrl}/QueryPayerInfo`,
{
IdentificationType: 'NIT',
gface: 'Digifact', // or extract from config
payerId: data2?.split('|')[1] || taxId,
EmisorUser: username,
EmisorNIT: taxId,
},
{
headers: {
Authorization: token,
'Content-Type': 'application/json',
},
}
)
);

return response.data;
}

/**
* Convert XML to DTE JSON structure expected by RPAfelApi
*/
private convertXmlToDteJson(xmlContent: string, taxId: string): any {
// Parse XML and convert to DTE JSON structure
// This is a simplified version - you'll need to parse the XML properly
// You can use xml2js or similar library

// For now, return structure - you'll need to implement XML parsing
return {
ID: 'DatosCertificados',
DatosEmision: {
ID: 'DatosEmision',
// ... parse from XML
},
};
}

/**
* Transform RPAfelApi response to match certifier response format
*/
private transformResponse(rpaResponse: any): any {
return {
Codigo: rpaResponse.Codigo || 1,
Mensaje: rpaResponse.Mensaje || 'OK',
AcuseReciboSAT: rpaResponse.AcuseReciboSAT,
Autorizacion: rpaResponse.Autorizacion_Text || rpaResponse.Autorizacion,
Serie: rpaResponse.Autorizacion_Serie,
NUMERO: rpaResponse.Autorizacion_Numero,
Fecha_DTE: rpaResponse.FechaHoraCertificacion,
Fecha_de_certificacion: rpaResponse.FechaHoraCertificacion,
NITCertificador: rpaResponse.NITCertificador,
NombreCertificador: rpaResponse.NombreCertificador,
ResponseDATA1: rpaResponse.ResponseDATA1, // Base64 XML if available
};
}
}

Phase 2: Add XML to DTE JSON Converter

File: apps/backend/src/fel/application/xml-to-dte-json.service.ts

import { Injectable } from '@nestjs/common';
import { parseString } from 'xml2js';
import type { Invoice } from '@/fel/domain/fel.interface';

@Injectable()
export class XmlToDteJsonService {
/**
* Convert Invoice object to DTE JSON structure
*/
convertInvoiceToDteJson(invoice: Invoice, rpaUUID: string): any {
return {
ID: 'DatosCertificados',
DatosEmision: {
ID: 'DatosEmision',
DatosGenerales: {
Tipo: invoice.generalData.type,
FechaHoraEmision: invoice.generalData.dateTimeIssue,
CodigoMoneda: invoice.generalData.currencyCode || 'GTQ',
rpaUUID: rpaUUID,
rpaDE_Empresa: this.mapProviderName(invoice.felProviderData.providerName),
rpaCertificador_Usuario: '', // Will be set from business config
rpaCertificador_Clave: '', // Will be set from business config
rpaFisco_Usuario: '',
},
Emisor: {
NITEmisor: invoice.issuerData.taxId,
NombreEmisor: invoice.issuerData.taxName,
CodigoEstablecimiento: invoice.issuerData.establishmentCode,
NombreComercial: invoice.issuerData.commercialName,
AfiliacionIVA: invoice.issuerData.vatAffiliationCode,
DireccionEmisor: {
Direccion: invoice.issuerData.addressData.address,
CodigoPostal: invoice.issuerData.addressData.postalCode,
Municipio: invoice.issuerData.addressData.municipality,
Departamento: invoice.issuerData.addressData.department,
Pais: invoice.issuerData.addressData.country,
},
},
Receptor: {
NombreReceptor: invoice.receiverData.taxName,
IDReceptor: invoice.receiverData.taxId,
TipoEspecial: invoice.receiverData.taxpayerType || 'NIT',
DireccionReceptor: {
Direccion: invoice.receiverData.addressData.address,
CodigoPostal: invoice.receiverData.addressData.postalCode,
Municipio: invoice.receiverData.addressData.municipality,
Departamento: invoice.receiverData.addressData.department,
Pais: invoice.receiverData.addressData.country,
},
},
Frases: {
Frase: invoice.phrases.map((phrase) => ({
TipoFrase: phrase.phraseType,
CodigoEscenario: phrase.scenarioCode,
})),
},
Items: {
Item: invoice.items.map((item, index) => ({
NumeroLinea: (index + 1).toString(),
BienOServicio: item.goodOrService,
Cantidad: item.quantity.toString(),
UnidadMedida: item.unitOfMeasure,
Descripcion: item.name,
PrecioUnitario: item.unitPrice.toFixed(2),
Precio: item.amount.toFixed(2),
Descuento: item.discount.toFixed(2),
Total: (item.amount - item.discount).toFixed(2),
Impuestos: {
Impuesto: item.taxes.map((tax) => ({
NombreCorto: tax.shortName,
CodigoUnidadGravable: tax.taxableUnitCode,
MontoGravable: tax.taxableAmount.toFixed(2),
MontoImpuesto: tax.taxAmount.toFixed(2),
})),
},
})),
},
Totales: {
GranTotal: invoice.total.toFixed(2),
TotalImpuestos: {
TotalImpuesto: this.calculateTotalTaxes(invoice.items).map((tax) => ({
NombreCorto: tax.name,
TotalMontoImpuesto: tax.amount.toFixed(2),
})),
},
},
},
extra: {
subDomain: invoice.addendumData?.version || '',
TipoEspecial: invoice.receiverData.taxpayerType || 'NIT',
},
};
}

private mapProviderName(providerName: string): string {
const mapping: Record<string, string> = {
digifact: 'Digifact',
infile: 'Infile',
fegora: 'Fegora',
guatefactura: 'GuateFactura',
};
return mapping[providerName.toLowerCase()] || 'Digifact';
}

private calculateTotalTaxes(items: InvoiceItem[]): Array<{ name: string; amount: number }> {
const taxMap = new Map<string, number>();

items.forEach((item) => {
item.taxes.forEach((tax) => {
const current = taxMap.get(tax.shortName) || 0;
taxMap.set(tax.shortName, current + tax.taxAmount);
});
});

return Array.from(taxMap.entries()).map(([name, amount]) => ({ name, amount }));
}
}

Phase 3: Update FelService

Update: apps/backend/src/fel/application/fel.service.ts

async certifyDocument({
taxId,
document,
}: ICertifyDocumentParameters): Promise<undefined> {
const { businessData, felProviderData } = document;

try {
// Step 1: Fetch business data
const business = await this.businessesRepository.findById(businessData.id);
if (!business) {
throw new Error('Business not found');
}

// Step 2: Check if using RPAfelApi
const useRpaFelApi = process.env.USE_RPA_FEL_API === 'true' ||
felProviderData.providerName === 'rpafelapi';

if (useRpaFelApi) {
return await this.certifyViaRpaFelApi(document, business, taxId);
}

// Step 3: Use direct certifier (existing logic)
// ... existing code ...
} catch (error) {
// ... error handling ...
}
}

private async certifyViaRpaFelApi(
document: Invoice,
business: SelectableBusiness,
taxId: string
): Promise<undefined> {
// Extract certifier config
const { felCertifier, encryptedFelToken } =
this.extractFelCertifierConfig(business.felCertifierConfig);

// Get certifier credentials
const certifierUser = this.getCertifierUser(business, felCertifier);
const certifierPassword = this.cryptoService.decrypt(encryptedFelToken);

// Convert Invoice to DTE JSON
const rpaUUID = document.addendumData.internalReference; // sale.id
const dteJson = this.xmlToDteJsonService.convertInvoiceToDteJson(document, rpaUUID);

// Add certifier credentials to DTE JSON
dteJson.DatosEmision.DatosGenerales.rpaCertificador_Usuario = certifierUser;
dteJson.DatosEmision.DatosGenerales.rpaCertificador_Clave = certifierPassword;

// Get RPAfelApi provider
const provider = this.providerService.getProvider('rpafelapi');

// Certify via RPAfelApi
const result = await provider.certifyDocument({
taxId,
xmlContent: '', // Not needed for RPAfelApi
apiUrl: '', // Not needed
token: '', // RPAfelApi handles auth internally
});

return result;
}

Phase 4: Update ProviderService

Update: apps/backend/src/fel/application/provider.service.ts

getProvider(providerName: string): ElectronicCertificationProvider {
switch (providerName.toLowerCase()) {
case 'digifact':
return this.providerDigifactService;
case 'infile':
return this.providerInfileService;
case 'rpafelapi':
return this.providerRpaFelApiService; // Add this
default:
throw new Error(`Unknown provider: ${providerName}`);
}
}

Phase 5: Update FelModule

Update: apps/backend/src/fel/fel.modules.ts

import { ProviderRpaFelApiService } from '@/fel/infrastructure/provider-rpafelapi.service';
import { XmlToDteJsonService } from '@/fel/application/xml-to-dte-json.service';

@Module({
// ... existing imports ...
providers: [
FelService,
ProviderService,
XmlConversionService,
XmlToDteJsonService, // Add this
ProviderDigifactService,
ProviderInfileService,
ProviderRpaFelApiService, // Add this
],
// ...
})
export class FelModule {}

Phase 6: Add Environment Configuration

Update: .env files

# Use RPAfelApi instead of direct certifiers
USE_RPA_FEL_API=true

# RPAfelApi URLs
RPA_FEL_API_URL=https://fel.rpapos.com/api/fel
# or for dev:
# RPA_FEL_API_URL=https://fel-dev.rpapos.com/api/fel

🎯 Benefits of This Approach

1. Immediate Value

  • ✅ Certificate caching (built-in)
  • ✅ "Already signed" handling (automatic)
  • ✅ All document types supported
  • ✅ Production-tested

2. Simpler Code

  • ✅ No need to implement caching
  • ✅ No need to handle 406 errors
  • ✅ Less code to maintain
  • ✅ Focus on business logic

3. Better Features

  • ✅ Void certificate endpoint
  • ✅ Query payer info
  • ✅ Email sending
  • ✅ Pub/Sub events

4. Future-Proof

  • ✅ Centralized FEL service
  • ✅ Easy to add new features
  • ✅ Shared across systems
  • ✅ Consistent behavior

🔄 Migration Strategy

Phase 1: Parallel Implementation (Week 1)

  • ✅ Add RPAfelApi provider
  • ✅ Keep existing direct certifiers
  • ✅ Add feature flag: USE_RPA_FEL_API
  • ✅ Test with sample documents

Phase 2: Gradual Rollout (Week 2)

  • ✅ Enable for test businesses
  • ✅ Monitor performance
  • ✅ Compare results
  • ✅ Fix any issues

Phase 3: Full Migration (Week 3)

  • ✅ Enable for all businesses
  • ✅ Keep direct certifiers as fallback
  • ✅ Monitor and optimize
  • ✅ Document changes

📊 Comparison: Before vs After

FeatureDirect CertifiersRPAfelApi
Certificate Caching❌ No✅ Yes (Infile)
Already Signed Handling❌ Manual✅ Automatic
Void Certificate❌ Not implemented✅ Yes
Query Payer Info⚠️ Partial✅ Yes
Email Sending❌ Not implemented✅ Yes
Code Complexity⚠️ Medium✅ Low
Maintenance⚠️ You maintain✅ Centralized

🚀 Quick Start

1. Install Dependencies

npm install xml2js @types/xml2js

2. Create Provider Service

Copy the ProviderRpaFelApiService code above.

3. Create XML to DTE JSON Service

Copy the XmlToDteJsonService code above.

4. Update FelService

Add the certifyViaRpaFelApi method.

5. Update Configuration

Add environment variables:

USE_RPA_FEL_API=true
RPA_FEL_API_URL=https://fel.rpapos.com/api/fel

6. Test

// Test with a sample invoice
const result = await felService.certifyDocument({
taxId: '44653948',
document: sampleInvoice,
});

🎯 Next Steps

  1. Implement RPAfelApi Provider - Use code above
  2. Add XML to DTE JSON Converter - Convert Invoice to DTE JSON
  3. Update FelService - Add RPAfelApi path
  4. Test Thoroughly - Test with real documents
  5. Enable Feature Flag - Gradual rollout
  6. Monitor Performance - Track metrics
  7. Full Migration - Enable for all

💡 Key Insights

  1. RPAfelApi is Production-Ready - Already deployed and tested
  2. All Features Included - Caching, error handling, void, etc.
  3. Faster Implementation - No need to build everything
  4. Better Long-term - Centralized service, easier maintenance
  5. Supports All Document Types - FACT, NCRE, NDEB, ANULACION

Last Updated: 2025-12-18 Status: Implementation Guide - Ready to Code!