Skip to main content

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!