Skip to main content

RPAfelApi Architecture Analysis

πŸ“Š Overview​

RPAfelApi is a production-ready .NET/C# API for FEL certification that demonstrates several excellent patterns we should consider adopting in FlowPOS.


🎯 Key Features & Patterns​

1. XSD Code Generation βœ…β€‹

What they do:

xsd GT_Documento-0.1.2.xsd xmldsig-core-schema.xsd /c /out:../Models /namespace:GT_Documento

Analysis:

  • Uses xsd.exe (Microsoft tool) to generate C# classes from XSD
  • Same approach as FELgt repository
  • Ensures 100% schema compliance

For FlowPOS:

  • βœ… Confirms XSD code generation is the right approach
  • βœ… We should do the same for TypeScript
  • βœ… Validates our FELgt contribution idea

2. Certificate Caching 🎯 EXCELLENT PATTERN​

Their Flow:

Request β†’ Check if rpaUUID exists in storage β†’ 
If FOUND: Return cached certificate (200)
If NOT FOUND: Sign β†’ Save β†’ Publish β†’ Return (200)

Why This is Brilliant:

  1. Performance:

    • Avoids re-signing same document
    • Faster response times
    • Reduces certifier API calls
  2. Cost:

    • Certifiers may charge per request
    • Caching saves money
  3. Idempotency:

    • Same request = same response
    • Prevents duplicate certifications
    • Better error recovery

For FlowPOS - Implementation:

// Add to FelService
async certifyDocument(params: ICertifyDocumentParameters): Promise<undefined> {
const { document } = params;
const internalReference = document.addendumData.internalReference; // sale.id

// Check cache first
const cached = await this.felCertificationCacheRepository.findByInternalReference(
internalReference
);

if (cached && cached.status === 'certified') {
return cached.certifierResponse; // Return cached
}

// Not cached, proceed with certification
const result = await this.provider.certifyDocument({...});

// Save to cache
await this.felCertificationCacheRepository.create({
internalReference,
certifierResponse: result,
status: 'certified',
// ... other fields
});

return result;
}

Database Table:

CREATE TABLE fel_certification_cache (
id UUID PRIMARY KEY,
internal_reference VARCHAR NOT NULL UNIQUE, -- sale.id
business_id UUID NOT NULL,
document_type VARCHAR, -- FACT, NCRE, NDEB, ANULACION
certifier_response JSONB NOT NULL,
status VARCHAR, -- 'certified', 'failed', 'pending'
created_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ, -- Optional: cache expiration
INDEX idx_internal_reference (internal_reference)
);

3. "Already Signed" Error Handling 🎯 IMPORTANT​

Their Flow:

FirmarFactura β†’ 406 (already signed) β†’ 
FirmarFacturaGet (fetch from GFACE) β†’
Save to DB β†’ Publish β†’ 200

Why This Matters:

  • Scenario: Document was already certified (maybe by another system, retry, etc.)
  • Problem: Certifier returns 406 "Already signed"
  • Solution: Fetch the existing certificate instead of failing

For FlowPOS:

async certifyDocument(params: ICertifyDocumentParameters): Promise<undefined> {
try {
return await this.provider.certifyDocument({...});
} catch (error) {
// Check if it's "already signed" error
if (error.response?.status === 406 ||
error.response?.data?.Codigo === 406) {

// Try to fetch existing certificate
const existing = await this.fetchExistingCertificate(
params.document.addendumData.internalReference
);

if (existing) {
// Cache it and return
await this.cacheCertificate(existing);
return existing;
}
}
throw error;
}
}

4. Event Publishing (Pub/Sub) 🎯 GOOD PATTERN​

Their Flow:

Save to DB β†’ Publish Topic β†’ 200

Why This is Good:

  1. Decoupling:

    • Certification doesn't block other processes
    • Multiple subscribers can react
    • Async processing
  2. Scalability:

    • Can handle high volume
    • Queue-based processing
    • Better error recovery

For FlowPOS:

You already have event-driven architecture! Just need to ensure:

  • βœ… Events are published after certification
  • βœ… Multiple handlers can subscribe
  • βœ… Consider adding Pub/Sub for distributed systems

Current Implementation:

// You already do this!
this.eventEmitter.emit(
OnDocumentCertifiedEvent.eventName,
new OnDocumentCertifiedEvent(saleEventRecord, certifyDocumentResult),
);

Enhancement - Add Pub/Sub:

// After certification
await this.pubSubService.publish('fel.document.certified', {
saleId: saleEventRecord.id,
felUuid: result.Autorizacion,
certifier: felCertifier,
timestamp: new Date(),
});

5. Internal Reference (rpaUUID)​

Their Pattern:

  • Use rpaUUID as unique identifier
  • Maps to your internalReference (sale.id)
  • Used for caching and lookups

For FlowPOS:

  • βœ… You already use internalReference: saleEventRecord.id
  • βœ… Perfect for caching key
  • βœ… Good for idempotency

πŸ”„ Comparison: RPAfelApi vs FlowPOS​

FeatureRPAfelApiFlowPOSRecommendation
XSD Generationβœ… C# classes❌ ManualAdd TypeScript generation
Certificate Cachingβœ… Yes❌ NoAdd this!
Already Signed Handlingβœ… Yes❌ NoAdd this!
Event Publishingβœ… Pub/Subβœ… EventsKeep events, consider Pub/Sub
Error Handlingβœ… Comprehensiveβœ… GoodEnhance with 406 handling
Idempotencyβœ… Via cache⚠️ PartialAdd caching for full idempotency

Priority 1: Certificate Caching ⭐⭐⭐​

Why:

  • Prevents duplicate certifications
  • Faster responses
  • Cost savings
  • Better error recovery

Implementation:

  1. Create fel_certification_cache table
  2. Check cache before certifying
  3. Save to cache after certification
  4. Use internalReference (sale.id) as key

Priority 2: "Already Signed" Handling ⭐⭐⭐​

Why:

  • Handles edge cases gracefully
  • Prevents failures on retries
  • Better user experience

Implementation:

  1. Catch 406 errors
  2. Fetch existing certificate from certifier
  3. Cache and return

Priority 3: Enhanced Error Logging ⭐⭐​

Why:

  • Better debugging
  • Track certification failures
  • Monitor certifier issues

Implementation:

  1. Save errors to database
  2. Include full request/response
  3. Track retry attempts

πŸ“‹ Implementation Plan​

Phase 1: Certificate Caching​

Migration:

CREATE TABLE fel_certification_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
business_id UUID NOT NULL REFERENCES business(id),
internal_reference VARCHAR NOT NULL, -- sale.id
document_type VARCHAR, -- FACT, NCRE, NDEB, ANULACION
original_invoice_uuid VARCHAR, -- For NCRE/NDEB/ANULACION
certifier VARCHAR NOT NULL,
certifier_response JSONB NOT NULL,
status VARCHAR NOT NULL, -- 'certified', 'failed', 'pending'
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
UNIQUE(internal_reference, document_type)
);

CREATE INDEX idx_fel_cache_internal_ref ON fel_certification_cache(internal_reference);
CREATE INDEX idx_fel_cache_business ON fel_certification_cache(business_id);

Service Update:

// FelService.certifyDocument()
async certifyDocument(params: ICertifyDocumentParameters): Promise<undefined> {
const cacheKey = this.getCacheKey(params.document);

// Check cache
const cached = await this.felCacheRepository.findByKey(cacheKey);
if (cached?.status === 'certified') {
this.logger.log(`Returning cached certificate for ${cacheKey}`);
return cached.certifierResponse;
}

// Proceed with certification...
const result = await this.provider.certifyDocument({...});

// Save to cache
await this.felCacheRepository.create({
...cacheKey,
certifierResponse: result,
status: 'certified',
});

return result;
}

Phase 2: Already Signed Handling​

Update Provider Services:

async certifyDocument(params: IProviderCertifyDocumentParameters): Promise<undefined> {
try {
return await this.httpService.post(...);
} catch (error) {
// Check for "already signed"
if (this.isAlreadySignedError(error)) {
// Fetch existing certificate
return await this.fetchExistingCertificate(params);
}
throw error;
}
}

private isAlreadySignedError(error: any): boolean {
return error.response?.status === 406 ||
error.response?.data?.Codigo === 406 ||
error.response?.data?.Mensaje?.includes('ya certificado');
}

private async fetchExistingCertificate(params: IProviderCertifyDocumentParameters): Promise<undefined> {
// Call certifier's "get certificate" endpoint
// Implementation depends on certifier API
}

πŸ’‘ Key Insights​

1. Caching is Critical​

  • RPAfelApi shows production-grade caching
  • Prevents duplicate certifications
  • Essential for high-volume systems

2. Error Handling Matters​

  • "Already signed" is a real scenario
  • Need graceful handling
  • Fetch existing vs fail

3. Idempotency via Internal Reference​

  • Use internalReference (sale.id) as cache key
  • Same sale = same certificate
  • Prevents duplicates

4. Async Processing​

  • Pub/Sub for scalability
  • Your event system is good
  • Consider adding message queue

🎯 Action Items​

Immediate (High Value)​

  1. Add Certificate Caching:

    • Create fel_certification_cache table
    • Update FelService to check cache
    • Save results to cache
  2. Handle "Already Signed":

    • Catch 406 errors
    • Fetch existing certificate
    • Return gracefully

Short Term​

  1. Enhanced Error Logging:

    • Save errors to database
    • Track retry attempts
    • Monitor certifier health
  2. Cache Expiration:

    • Optional: Add TTL to cache
    • Refresh stale certificates
    • Cleanup old entries

Long Term​

  1. Pub/Sub Integration:
    • Add message queue (Redis, Pub/Sub)
    • Async certification processing
    • Better scalability

πŸ“š References​

  • RPAfelApi: (Internal repository - C# .NET implementation)
  • XSD Generation: Same approach as FELgt
  • Certificate Caching: Production best practice
  • Error Handling: Real-world scenario handling

Last Updated: 2025-12-18
Status: Analysis Complete - Ready for Implementation