Skip to main content

Nexus Oracle Fusion Purchase Order Worker (Service Layer + Alpura-SDK)

9.1 Responsabilidad​

Consumir mensajes de Pub/Sub (nexus.purchase-order.requested / retry) y crear la OC en Oracle Fusion Procurement. La integración se encapsula mediante interfaces Java (PurchaseOrderCreationService) en la capa Facade e implementaciones en la capa Service usando Alpura-SDK (HTTP clients, OAuth client credentials, Resilience4j, OpenTelemetry tracing, serialización, secrets). No se usa la terminología Port/Adapter de Hexagonal.

9.2 Ficha técnica del componente​

AspectoDetalle
ResponsabilidadConsumir, idempotencia, transformación canónico→Fusion, invocación REST, actualización estado, retry/DLQ, auditoría.
EntradasEnvelope Pub/Sub (JSON).
SalidasEstado en PostgreSQL + audit_event + datos Oracle (OC number).
DependenciasPostgreSQL, Oracle Fusion REST API, Secret Manager (credenciales Fusion), IdP token endpoint.
Datos administradospurchase_order, purchase_order_line, transaction_status_history, integration_error, processed_event.
APIsSin APIs HTTP; consumer suscripción (Spring Cloud GCP PubSubMessageController en capa WEB equivalente).
Eventosaudit_event varios tipos.
EstadosTransiciona QUEUED → PROCESSING → CREATED/FAILED/RETRY_PENDING/DLQ.
ErroresTaxonomía: TEMPORARY_ERROR, INTEGRATION_ERROR, BUSINESS_ERROR, TECHNICAL, NON_RETRYABLE.
PatronesIdempotent Consumer, Service Facade + Alpura-SDK (Resilience4j CB/Bulkhead/Retry/Timelimiter).
SeguridadCredenciales por partner; scopes; rotation secrets TBD.
ObservabilidadLatencia Fusion, reintentos, DLQ, hist por partner.

9.3 Diseño por capas (Arquitectura Base Backend Alpura)​

CapaClases / InterfacesMétodos (nomenclatura Alpura)
WEB (Listener Pub/Sub)PubSubMessageController (Spring Cloud GCP)onRequestedMessage(message), onRetryMessage(message), onDlqMessage(message)
Facade (Service Interfaces)PurchaseOrderCreationService (interface)createPurchaseOrder(request) (1 parámetro Value Object CreatePurchaseOrderServiceRequest)
Service (implementaciones)OracleFusionPurchaseOrderService, FusionTokenResolver, FusionMappingServicePúblicos: createPurchaseOrder()
Privados (prefijo _): _getFusionAccessToken(), _resolveMappingsForPartner(), _mapCanonicalToFusionRequest(), _invokeFusionCreatePo(), _classifyFusionResponseError(), _appendAudit(), _registerProcessedEvent()
PersistencePurchaseOrderRepository, PurchaseOrderLineRepository, ProcessedEventRepository, TransactionStatusRepository, IntegrationErrorRepository, AuditRepositoryPúblicos: findByRequestId(), saveRequestAndOutboxTx(), findByIdempotencyKey(), saveIntegrationError(), markProcessed()
Privados (en impl custom): _mapToEntity(), _buildCriteriaQuery()
Commonscommons.valueobjects (FusionClientConfig, ScopeContext, Correlation, OraclePoNumber, FusionPoId)
commons.exceptions (FusionAuthenticationException, FusionBusinessException, FusionTemporaryException, FusionIntegrationException)
commons.constants (FusionHeaderKeys, FusionStatusCodes, ResilienceDefaults)

9.4 Flujo interno (pseudocódigo con nomenclatura Alpura)​

// WEB / Listener equivalente (Spring Cloud GCP)
@RestController
public class PubSubMessageController {

private final PurchaseOrderCreationService creationService; // Facade interface

@PubSubListener(subscription = "nexus.purchase-order.requested.sub")
public void onRequestedMessage(PubsubMessage message) {
Envelope envelope = _parseEnvelope(message);
// 1) Idempotent consumer
if (processedEventRepo.exists(envelope.getEventId())) {
acknowledge(message); return;
}
// 2) carga request + canonical
PurchaseOrderRequest req = purchaseOrderRepo.findByRequestId(envelope.getRequestId())
.orElseThrow(() -> new NexusNotFoundException("requestId", envelope.getRequestId()));
// 3) valida estado permisible
if (!ALLOWED_STATUSES.contains(req.getStatus())) {
processedEventRepo.markProcessed(envelope.getEventId(), DUPLICATE_SKIPPED);
acknowledge(message); return;
}
txHelper.executeInTx(() -> {
req.transitionStatus(PROCESSING, "worker starting fusion");
statusRepo.appendTransition(req.getRequestId(), PROCESSING, "fusion-worker", null);
});

// 4) invoca capa Service mediante interface Facade (1 param Value Object)
CreatePurchaseOrderServiceRequest serviceRequest = _toServiceRequest(req, envelope);
try {
CreatePurchaseOrderServiceResponse result = creationService.createPurchaseOrder(serviceRequest);
_handleCreated(req, envelope, result);
} catch (NexusTemporaryException e) {
_handleTemporary(req, envelope, e);
return;
} catch (NexusNonRetryableException e) {
_handleNonRetryable(req, envelope, e);
return;
}
acknowledge(message);
}

private Envelope _parseEnvelope(PubsubMessage m) { /* ... */ }
private CreatePurchaseOrderServiceRequest _toServiceRequest(PurchaseOrderRequest r, Envelope env) { /* agrupa */ }
private void _handleCreated(PurchaseOrderRequest r, Envelope e, CreatePurchaseOrderServiceResponse res) { /* ... */ }
private void _handleTemporary(PurchaseOrderRequest r, Envelope e, NexusTemporaryException ex) { /* pub retry topic */ }
private void _handleNonRetryable(PurchaseOrderRequest r, Envelope e, NexusNonRetryableException ex) { /* pub DLQ */ }
}

9.5 Integración con Oracle Fusion (conceptual)​

TBD — pendiente de confirmar con documentación oficial de Oracle Fusion: endpoint específico, versión API, nombre de recurso, IDs de BU/LE, esquema de payload, estructura success response y códigos error.

9.5.1 Puntos acordados TO-BE (antes de confirmación con Fusion)​

AspectoAcuerdo provisional
Estilo APIREST (documentado en docs Oracle Fusion Procurement: Purchase Orders / requisitions / import batch). TBD.
AutenticaciónOAuth 2.0 client credentials contra token endpoint de Fusion. TBD. Token obtenido por FusionTokenResolver usando Alpura-SDK + Secret Manager.
TimeoutResilience4j TimeLimiter: TBD 30s connect / TBD 90s read. Configurado por medio de Alpura-SDK.webClientBuilder().
RetriesExponential backoff TBD (initial=2s, factor=2, max=30s, attempts=3 en _invokeFusionCreatePo; luego delegar a Pub/Sub retry policy).
Circuit BreakerResilience4j CB por partner + endpoint. Sliding window count. TBD umbrales.
BulkheadSemáforo por partner (TBD maxConcurrentCalls) para proteger Fusion.
Errores funcionalesCualquier respuesta 4xx con error code de negocio → NON_RETRYABLE → FAILED/DLQ (según configuración).
Errores técnicos5xx / timeout / socket exception → TEMPORARY_ERROR → retry / DLQ.
MappingRealizado en FusionMappingService; mapeos específicos por partnerId via partner_configuration JSONB.
Idempotencia FusionPasar externalReference / requestId como campo de request id si Fusion admite (ej: Attribute1, campo flexfield, o partnerReference). TBD nombre campo exacto.

9.5.2 Interface Service Facade y implementación (Alpura-SDK)​

// Facade: Service Interface (capa Facade)
public interface PurchaseOrderCreationService {
// 1 param DT (agrupa canonical + context) -> 0-3 params
CreatePurchaseOrderServiceResponse createPurchaseOrder(CreatePurchaseOrderServiceRequest request)
throws NexusTemporaryException, NexusNonRetryableException;
}

// Value Object (Commons · Value Objects) - reduce params
public record CreatePurchaseOrderServiceRequest(
CanonicalPurchaseOrder canonical,
ScopeContext scope,
Correlation correlation,
FusionClientConfig fusionConfig
) {}

// Service impl - capa Service (implementa facade; usa Alpura-SDK)
@Service
public class OracleFusionPurchaseOrderService implements PurchaseOrderCreationService {

private final AlpuraSdk alpuraSdk; // Libreria Alpura morada
private final FusionMappingService mappingService;
private final AuditWriter auditWriter;

@Override
public CreatePurchaseOrderServiceResponse createPurchaseOrder(CreatePurchaseOrderServiceRequest req) {
FusionClientConfig cfg = req.fusionConfig() != null
? req.fusionConfig()
: _resolveFusionConfig(req.scope());
String accessToken = _getFusionAccessToken(cfg);
Map<String,Object> fusionPayload = _mapCanonicalToFusionRequest(req.canonical(), cfg);
FusionHttpResponse response = _invokeFusionCreatePo(cfg, accessToken, fusionPayload);
if (!response.isSuccessful()) {
var classified = _classifyFusionResponseError(response, req);
_appendAudit(req, classified);
throw classified.exception();
}
FusionPoId fusionPoId = response.extractPoId();
_registerProcessedEvent(req.correlation().eventId(), fusionPoId);
_appendAudit(req, FusionOutcome.builder().poId(fusionPoId).build());
return new CreatePurchaseOrderServiceResponse(req.correlation().requestId(), fusionPoId);
}

private FusionClientConfig _resolveFusionConfig(ScopeContext scope) { /* ... */ }
private String _getFusionAccessToken(FusionClientConfig cfg) { /* Alpura-SDK + Secret Manager */ }
private Map<String,Object> _mapCanonicalToFusionRequest(CanonicalPurchaseOrder canonical, FusionClientConfig cfg) {
return mappingService.toFusionPayload(canonical, cfg);
}
private FusionHttpResponse _invokeFusionCreatePo(
FusionClientConfig cfg,
String token,
Map<String,Object> payload
) {
var webClient = alpuraSdk.httpClientBuilder()
.withBaseUrl(cfg.baseUrl())
.withAuthentication(OAuth2Bearer.of(token))
.withCircuitBreaker(CircuitBreakerConfig.forName("fusion-" + cfg.partnerId()))
.withBulkhead(BulkheadConfig.semaphore("fusion-" + cfg.partnerId()))
.withTimeLimiter(Duration.ofSeconds(90))
.withTracing(propagateTraceparent())
.build();
return webClient.post(cfg.createPoPath())
.body(payload)
.retrieve()
.toEntity(FusionHttpResponse.class);
}
private ClassifiedFusionError _classifyFusionResponseError(FusionHttpResponse resp, CreatePurchaseOrderServiceRequest req) { /* ... */ }
private void _appendAudit(CreatePurchaseOrderServiceRequest req, FusionOutcome outcome) { /* ... */ }
private void _registerProcessedEvent(UUID eventId, FusionPoId poId) { /* ... */ }
}

9.6 Futuras implementaciones Service (extensibilidad)​

En lugar de “adapters del puerto”, se agregan nuevas implementaciones de la interface PurchaseOrderCreationService y se selecciona la instancia correcta por medio de @Qualifier por partner / sourceSystem:

Selección: PurchaseOrderServiceFactory.getForPartner(partnerId) devuelve la implementación correcta. Todas usan Alpura-SDK para su cliente HTTP, auth y resiliencia.