RPAfelApi Integration Implementation Guide
π― Updated Recommendation: Use RPAfelApi ββββ
After reviewing the complete API documentation, RPAfelApi is the clear winner:
Why Use RPAfelApi Nowβ
-
β Already Production-Ready
- Deployed at:
https://fel.rpapos.com/api/fel - Production-tested
- Comprehensive error handling
- Deployed at:
-
β 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
-
β Faster Implementation
- No need to build caching
- No need to implement error handling
- Just integrate the API
-
β 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β
| Feature | Direct Certifiers | RPAfelApi |
|---|---|---|
| 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β
- Implement RPAfelApi Provider - Use code above
- Add XML to DTE JSON Converter - Convert Invoice to DTE JSON
- Update FelService - Add RPAfelApi path
- Test Thoroughly - Test with real documents
- Enable Feature Flag - Gradual rollout
- Monitor Performance - Track metrics
- Full Migration - Enable for all
π‘ Key Insightsβ
- RPAfelApi is Production-Ready - Already deployed and tested
- All Features Included - Caching, error handling, void, etc.
- Faster Implementation - No need to build everything
- Better Long-term - Centralized service, easier maintenance
- Supports All Document Types - FACT, NCRE, NDEB, ANULACION
Last Updated: 2025-12-18 Status: Implementation Guide - Ready to Code!