INTEGRATIONS ARCHITECTURE
Arquitetura de Integrações do Sport Tech Club
Documentação técnica completa de todas as integrações externas e internas do sistema.
Versão: 1.0.0
Última Atualização: 2026-01-09
Responsável: Arquitetura de Software
Índice
- Visão Geral
- Princípios Arquiteturais
- Integrações Implementadas
- Integrações Planejadas
- Padrões de Integração
- Segurança
- Resiliência e Confiabilidade
- Monitoramento
Visão Geral
Mapa de Integrações
┌─────────────────────────────────────────────────────────────────┐
│ SPORT TECH CLUB CORE │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Integration Layer (ACL) │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Adapter │ │Adapter │ │Adapter │ │Adapter │ │ │
│ │ │Ziggy │ │Payment │ │Keycloak│ │Comms │ ... │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Event Bus │ │
│ │ (RabbitMQ / Redis Pub/Sub) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Ziggy │ │Payment │ │Keycloak│ │WhatsApp│
│ API │ │Gateway │ │ SSO │ │ API │
└────────┘ └────────┘ └────────┘ └────────┘Categorias de Integração
| Categoria | Quantidade | Criticidade |
|---|---|---|
| Controle de Acesso | 1 | CRÍTICA |
| Pagamentos | 3 | CRÍTICA |
| Identidade & Auth | 1 | CRÍTICA |
| Comunicação | 4 | ALTA |
| Vídeo & Mídia | 1 | MÉDIA |
| IoT & Dispositivos | 5 | ALTA |
| Analytics | 3 | BAIXA |
| Ecosystem | 2 | ALTA |
| CRM/ERP | 2 | BAIXA |
Princípios Arquiteturais
1. Anti-Corruption Layer (ACL)
Protege o domínio do sistema de mudanças em sistemas externos.
// domain/integrations/ports/access-control.port.ts
export interface AccessControlPort {
checkIn(memberId: string, courtId: string): Promise<CheckInResult>;
checkOut(sessionId: string): Promise<CheckOutResult>;
getActiveSession(memberId: string): Promise<AccessSession | null>;
}
// infrastructure/integrations/ziggy/ziggy-access-control.adapter.ts
export class ZiggyAccessControlAdapter implements AccessControlPort {
constructor(
private readonly httpClient: HttpClient,
private readonly mapper: ZiggyMapper,
private readonly config: ZiggyConfig
) {}
async checkIn(memberId: string, courtId: string): Promise<CheckInResult> {
try {
// Chamada à API externa do Ziggy
const response = await this.httpClient.post<ZiggyCheckInResponse>(
`${this.config.baseUrl}/access/checkin`,
{
user_id: memberId,
court_id: courtId,
timestamp: new Date().toISOString()
},
{
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'X-Client-Id': this.config.clientId
}
}
);
// Traduz resposta externa para modelo de domínio
return this.mapper.toCheckInResult(response.data);
} catch (error) {
throw new AccessControlIntegrationError(
'Failed to check in via Ziggy',
error
);
}
}
}2. Dependency Inversion
// application/use-cases/check-in-member.use-case.ts
export class CheckInMemberUseCase {
constructor(
// Depende de abstração, não de implementação
private readonly accessControl: AccessControlPort,
private readonly memberRepo: MemberRepository,
private readonly eventBus: EventBus
) {}
async execute(command: CheckInCommand): Promise<CheckInResult> {
const member = await this.memberRepo.findById(command.memberId);
if (!member) {
throw new MemberNotFoundError(command.memberId);
}
if (!member.canCheckIn()) {
throw new InsufficientCreditsError(member.id);
}
// Usa abstração - não sabe que é Ziggy
const result = await this.accessControl.checkIn(
member.id,
command.courtId
);
member.deductCredit();
await this.memberRepo.save(member);
await this.eventBus.publish(
new MemberCheckedInEvent(member.id, command.courtId, result.sessionId)
);
return result;
}
}3. Circuit Breaker Pattern
// infrastructure/integrations/common/circuit-breaker.ts
export class CircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private lastFailureTime?: Date;
constructor(
private readonly threshold: number = 5,
private readonly timeout: number = 60000, // 1 min
private readonly halfOpenAttempts: number = 3
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
} else {
throw new CircuitBreakerOpenError('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = new Date();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
}
private shouldAttemptReset(): boolean {
if (!this.lastFailureTime) return false;
return Date.now() - this.lastFailureTime.getTime() > this.timeout;
}
}Integrações Implementadas
1. ZIGGY - Sistema de Controle de Acesso
Status: 🟢 Produção
Criticidade: CRÍTICA
Tipo: Síncrona (REST API)
Fornecedor: Ziggy Access Control
SLA: 99.9% uptime
Descrição
Sistema responsável por gerenciar o controle de acesso físico às quadras, incluindo check-in, check-out e rastreamento de consumo em tempo real.
Endpoints
| Método | Endpoint | Descrição |
|---|---|---|
| POST | /api/v1/access/checkin | Registra entrada em quadra |
| POST | /api/v1/access/checkout | Registra saída de quadra |
| GET | /api/v1/access/sessions/active | Lista sessões ativas |
| GET | /api/v1/access/sessions/{id} | Detalhes de sessão |
| POST | /api/v1/consumption/record | Registra consumo (bar, loja) |
| GET | /api/v1/consumption/{sessionId} | Histórico de consumo |
Eventos Recebidos
// Eventos que o Ziggy envia via webhook
interface ZiggyCheckInEvent {
event_type: 'access.checkin';
timestamp: string;
data: {
session_id: string;
user_id: string;
court_id: string;
checked_in_at: string;
device_id: string;
};
}
interface ZiggyCheckOutEvent {
event_type: 'access.checkout';
timestamp: string;
data: {
session_id: string;
checked_out_at: string;
duration_minutes: number;
consumption_total: number;
};
}
interface ZiggyConsumptionEvent {
event_type: 'consumption.recorded';
timestamp: string;
data: {
session_id: string;
item_id: string;
item_name: string;
quantity: number;
unit_price: number;
total_price: number;
};
}Adapter Implementation
// infrastructure/integrations/ziggy/ziggy.adapter.ts
export class ZiggyAdapter implements AccessControlPort {
private readonly circuitBreaker: CircuitBreaker;
private readonly retryPolicy: RetryPolicy;
constructor(
private readonly httpClient: HttpClient,
private readonly config: ZiggyConfig,
private readonly logger: Logger
) {
this.circuitBreaker = new CircuitBreaker(5, 60000);
this.retryPolicy = new RetryPolicy({
maxAttempts: 3,
backoff: 'exponential',
initialDelay: 1000
});
}
async checkIn(memberId: string, courtId: string): Promise<CheckInResult> {
return this.circuitBreaker.execute(async () => {
return this.retryPolicy.execute(async () => {
this.logger.info('Ziggy check-in', { memberId, courtId });
const response = await this.httpClient.post<ZiggyCheckInResponse>(
`${this.config.baseUrl}/access/checkin`,
{
user_id: memberId,
court_id: courtId,
timestamp: new Date().toISOString()
},
{
headers: this.getAuthHeaders(),
timeout: 5000
}
);
return {
sessionId: response.data.session_id,
checkedInAt: new Date(response.data.checked_in_at),
court: {
id: response.data.court.id,
name: response.data.court.name
}
};
});
});
}
private getAuthHeaders(): Record<string, string> {
return {
'Authorization': `Bearer ${this.config.apiKey}`,
'X-Client-Id': this.config.clientId,
'Content-Type': 'application/json'
};
}
}2. KEYCLOAK - Identity & Access Management
Status: 🟢 Produção
Criticidade: CRÍTICA
Tipo: OAuth 2.0 / OpenID Connect
Fornecedor: Red Hat Keycloak (Self-hosted)
SLA: 99.99% uptime
Descrição
Sistema de identidade centralizado que gerencia autenticação, autorização, SSO e multi-tenancy para todas as aplicações do ecossistema Sport Tech Club.
Fluxos de Autenticação
Authorization Code Flow (Web/Mobile)
// infrastructure/integrations/keycloak/keycloak-auth.adapter.ts
export class KeycloakAuthAdapter implements AuthenticationPort {
async authenticateService(
clientId: string,
clientSecret: string
): Promise<ServiceToken> {
const response = await this.httpClient.post<TokenResponse>(
`${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: 'openid profile'
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return {
accessToken: response.data.access_token,
expiresIn: response.data.expires_in,
tokenType: response.data.token_type
};
}
}Token Validation
// infrastructure/integrations/keycloak/keycloak-token-validator.ts
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';
export class KeycloakTokenValidator {
private jwksClient: jwksClient.JwksClient;
constructor(private readonly config: KeycloakConfig) {
this.jwksClient = jwksClient({
jwksUri: `${config.baseUrl}/realms/${config.realm}/protocol/openid-connect/certs`,
cache: true,
cacheMaxAge: 86400000 // 24 hours
});
}
async validate(token: string): Promise<TokenPayload> {
try {
const decoded = jwt.decode(token, { complete: true });
if (!decoded || typeof decoded === 'string') {
throw new InvalidTokenError('Invalid token format');
}
const key = await this.jwksClient.getSigningKey(decoded.header.kid);
const publicKey = key.getPublicKey();
const payload = jwt.verify(token, publicKey, {
issuer: `${this.config.baseUrl}/realms/${this.config.realm}`,
audience: this.config.clientId,
algorithms: ['RS256']
}) as jwt.JwtPayload;
return {
sub: payload.sub!,
email: payload.email,
name: payload.name,
roles: payload.realm_access?.roles || [],
tenantId: payload.tenant_id,
expiresAt: new Date(payload.exp! * 1000)
};
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new TokenExpiredError();
}
throw new InvalidTokenError(error.message);
}
}
}3. PAYMENT GATEWAYS - Processamento de Pagamentos
Status: 🟡 Implementação Parcial
Criticidade: CRÍTICA
Tipo: Síncrona (REST API) + Webhooks
Fornecedores: Stripe, Mercado Pago, PagSeguro
Strategy Pattern para Múltiplos Gateways
// domain/integrations/ports/payment-gateway.port.ts
export interface PaymentGatewayPort {
createPayment(command: CreatePaymentCommand): Promise<PaymentResult>;
capturePayment(paymentId: string): Promise<CaptureResult>;
refundPayment(paymentId: string, amount?: number): Promise<RefundResult>;
createSubscription(command: CreateSubscriptionCommand): Promise<Subscription>;
cancelSubscription(subscriptionId: string): Promise<void>;
getPaymentStatus(paymentId: string): Promise<PaymentStatus>;
}
// infrastructure/integrations/payment/payment-gateway.factory.ts
export class PaymentGatewayFactory {
constructor(
private readonly stripeAdapter: StripeAdapter,
private readonly mercadoPagoAdapter: MercadoPagoAdapter,
private readonly pagSeguroAdapter: PagSeguroAdapter
) {}
create(type: PaymentGatewayType): PaymentGatewayPort {
switch (type) {
case 'STRIPE':
return this.stripeAdapter;
case 'MERCADO_PAGO':
return this.mercadoPagoAdapter;
case 'PAGSEGURO':
return this.pagSeguroAdapter;
default:
throw new UnsupportedGatewayError(type);
}
}
}Stripe Integration
// infrastructure/integrations/payment/stripe/stripe.adapter.ts
import Stripe from 'stripe';
export class StripeAdapter implements PaymentGatewayPort {
private readonly stripe: Stripe;
constructor(private readonly config: StripeConfig) {
this.stripe = new Stripe(config.secretKey, {
apiVersion: '2023-10-16',
typescript: true
});
}
async createPayment(command: CreatePaymentCommand): Promise<PaymentResult> {
const paymentIntent = await this.stripe.paymentIntents.create({
amount: Math.round(command.amount * 100), // Centavos
currency: command.currency.toLowerCase(),
customer: command.customerId,
description: command.description,
metadata: command.metadata,
automatic_payment_methods: {
enabled: true
}
});
return {
id: paymentIntent.id,
status: this.mapStatus(paymentIntent.status),
amount: paymentIntent.amount / 100,
clientSecret: paymentIntent.client_secret!,
createdAt: new Date(paymentIntent.created * 1000)
};
}
async createSubscription(
command: CreateSubscriptionCommand
): Promise<Subscription> {
const subscription = await this.stripe.subscriptions.create({
customer: command.customerId,
items: [{ price: command.priceId }],
metadata: command.metadata,
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription'
}
});
return {
id: subscription.id,
status: this.mapSubscriptionStatus(subscription.status),
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000)
};
}
}4. COMMUNICATION CHANNELS - Canais de Comunicação
Status: 🟡 Implementação Parcial
Criticidade: ALTA
Tipo: Assíncrona (Webhooks) + API REST
WhatsApp Business API (Twilio)
// infrastructure/integrations/communication/whatsapp/twilio-whatsapp.adapter.ts
import twilio from 'twilio';
export class TwilioWhatsAppAdapter implements CommunicationPort {
private readonly client: twilio.Twilio;
constructor(private readonly config: TwilioConfig) {
this.client = twilio(config.accountSid, config.authToken);
}
async sendMessage(command: SendMessageCommand): Promise<MessageResult> {
const to = `whatsapp:${this.formatPhoneNumber(command.recipient)}`;
const from = `whatsapp:${this.config.phoneNumber}`;
const message = await this.client.messages.create({
to,
from,
body: command.content.text,
...(command.content?.mediaUrl && { mediaUrl: [command.content.mediaUrl] })
});
return {
messageId: message.sid,
status: this.mapStatus(message.status),
sentAt: new Date(message.dateCreated)
};
}
private formatPhoneNumber(phone: string): string {
let cleaned = phone.replace(/\D/g, '');
if (!cleaned.startsWith('55')) {
cleaned = '55' + cleaned;
}
return '+' + cleaned;
}
}Push Notifications (Firebase FCM)
// infrastructure/integrations/communication/push/firebase-fcm.adapter.ts
import * as admin from 'firebase-admin';
export class FirebaseFCMAdapter implements CommunicationPort {
private readonly messaging: admin.messaging.Messaging;
constructor(config: FirebaseConfig) {
const app = admin.initializeApp({
credential: admin.credential.cert(config.serviceAccount)
});
this.messaging = admin.messaging(app);
}
async sendMessage(command: SendMessageCommand): Promise<MessageResult> {
const message: admin.messaging.Message = {
token: command.recipient,
notification: {
title: command.content!.title,
body: command.content!.text,
imageUrl: command.content?.mediaUrl
},
data: command.metadata,
android: {
priority: 'high',
notification: {
channelId: 'default',
sound: 'default'
}
}
};
const messageId = await this.messaging.send(message);
return {
messageId,
status: MessageStatus.SENT,
sentAt: new Date()
};
}
}5. VIDEO SYSTEM - Sistema de Vídeo e Highlights
Status: 🔴 Planejado
Criticidade: MÉDIA
Tipo: Webhook + Streaming API
Fornecedor: A definir (Mux, Cloudflare Stream, AWS IVS)
Port & Adapter
// domain/integrations/ports/video-platform.port.ts
export interface VideoPlatformPort {
createLiveStream(command: CreateLiveStreamCommand): Promise<LiveStream>;
stopLiveStream(streamId: string): Promise<void>;
createHighlight(command: CreateHighlightCommand): Promise<Highlight>;
getVideoAsset(assetId: string): Promise<VideoAsset>;
getPlaybackUrl(assetId: string): Promise<string>;
deleteVideo(assetId: string): Promise<void>;
}
// infrastructure/integrations/video/mux/mux-video.adapter.ts
import Mux from '@mux/mux-node';
export class MuxVideoAdapter implements VideoPlatformPort {
private readonly mux: Mux;
constructor(config: MuxConfig) {
this.mux = new Mux(config.tokenId, config.tokenSecret);
}
async createLiveStream(
command: CreateLiveStreamCommand
): Promise<LiveStream> {
const liveStream = await this.mux.video.liveStreams.create({
playback_policy: ['public'],
new_asset_settings: {
playback_policy: ['public']
},
reconnect_window: 60,
latency_mode: 'low',
passthrough: JSON.stringify({
courtId: command.courtId,
sessionId: command.sessionId
})
});
return {
id: liveStream.id,
streamKey: liveStream.stream_key!,
playbackId: liveStream.playback_ids![0].id,
status: 'idle',
hlsUrl: `https://stream.mux.com/${liveStream.playback_ids![0].id}.m3u8`
};
}
}Physical Button Integration (IoT)
// infrastructure/integrations/iot/highlight-button.handler.ts
export class HighlightButtonHandler {
constructor(
private readonly mqttClient: MqttClient,
private readonly videoService: VideoService,
private readonly sessionRepo: SessionRepository
) {
this.subscribeToButtonEvents();
}
private subscribeToButtonEvents(): void {
this.mqttClient.subscribe('court/+/button/pressed', async (topic, message) => {
const courtId = this.extractCourtId(topic);
const buttonData = JSON.parse(message.toString());
await this.handleButtonPress(courtId, buttonData);
});
}
private async handleButtonPress(
courtId: string,
data: ButtonPressData
): Promise<void> {
const session = await this.sessionRepo.findActiveByCourtId(courtId);
if (!session || !session.liveStreamId) {
console.warn('No active session with live stream for court', courtId);
return;
}
const now = Date.now();
const streamStartTime = session.streamStartedAt.getTime();
const relativeTime = (now - streamStartTime) / 1000;
await this.videoService.createHighlight({
liveStreamId: session.liveStreamId,
startTime: Math.max(0, relativeTime - 10),
duration: 30,
sessionId: session.id,
courtId,
triggeredBy: 'button'
});
}
}6. IOT DEVICES - Dispositivos e Sensores
Status: 🟡 Implementação Parcial
Criticidade: ALTA
Tipo: MQTT + HTTP
Protocolos: MQTT, WebSocket, HTTP REST
MQTT Broker Architecture
// infrastructure/integrations/iot/mqtt-broker.service.ts
import * as mqtt from 'mqtt';
export class MqttBrokerService extends EventEmitter {
private client: mqtt.MqttClient;
constructor(private readonly config: MqttConfig) {
super();
this.connect();
}
private connect(): void {
this.client = mqtt.connect(this.config.brokerUrl, {
clientId: `sport-tech-club-${Math.random().toString(16).slice(2, 8)}`,
username: this.config.username,
password: this.config.password,
clean: true,
reconnectPeriod: 1000
});
this.client.on('connect', () => {
console.log('Connected to MQTT broker');
this.subscribeToTopics();
});
this.client.on('message', (topic, payload) => {
this.handleMessage(topic, payload);
});
}
private subscribeToTopics(): void {
const topics = [
'court/+/checkin',
'court/+/checkout',
'court/+/button/pressed',
'tablet/+/status',
'rfid/+/scan'
];
topics.forEach(topic => {
this.client.subscribe(topic, { qos: 1 });
});
}
}RFID/NFC Reader Integration
// infrastructure/integrations/iot/rfid-reader.adapter.ts
export class RfidReaderAdapter {
constructor(
private readonly mqttBroker: MqttBrokerService,
private readonly memberRepo: MemberRepository,
private readonly accessControl: AccessControlPort
) {
this.setupListeners();
}
private async handleRfidScan(msg: MqttMessage): Promise<void> {
const { card_id, reader_id } = msg.data;
const member = await this.memberRepo.findByRfidCard(card_id);
if (!member || !member.isActive()) {
await this.sendReaderResponse(reader_id, {
status: 'denied',
message: 'Acesso negado',
color: 'red'
});
return;
}
const courtId = this.getCourtIdByReader(reader_id);
try {
await this.accessControl.checkIn(member.id, courtId);
await this.sendReaderResponse(reader_id, {
status: 'granted',
message: `Bem-vindo, ${member.firstName}!`,
color: 'green'
});
} catch (error) {
await this.sendReaderResponse(reader_id, {
status: 'denied',
message: 'Erro ao registrar entrada',
color: 'red'
});
}
}
}7. ANTÓCTICA ECOSYSTEM - Integrações Internas
Status: 🟡 Planejamento
Criticidade: ALTA
Tipo: Event-Driven + REST API
Shared Wallet Integration
// domain/integrations/ports/wallet.port.ts
export interface WalletPort {
getBalance(userId: string): Promise<WalletBalance>;
createTransaction(command: CreateTransactionCommand): Promise<Transaction>;
transferBetweenApps(command: TransferCommand): Promise<Transfer>;
getTransactionHistory(userId: string): Promise<Transaction[]>;
}
// infrastructure/integrations/antoctica/wallet.adapter.ts
export class AntocticaWalletAdapter implements WalletPort {
async getBalance(userId: string): Promise<WalletBalance> {
const token = await this.authService.getServiceToken();
const response = await this.httpClient.get<WalletBalanceResponse>(
`${this.config.walletApiUrl}/users/${userId}/balance`,
{
headers: {
'Authorization': `Bearer ${token}`,
'X-Service-Id': 'sport-tech-club'
}
}
);
return {
available: response.data.available,
pending: response.data.pending,
currency: response.data.currency
};
}
async transferBetweenApps(command: TransferCommand): Promise<Transfer> {
const token = await this.authService.getServiceToken();
const response = await this.httpClient.post<TransferResponse>(
`${this.config.walletApiUrl}/transfers/inter-app`,
{
from_app: 'sport-tech-club',
to_app: command.targetApp,
user_id: command.userId,
amount: command.amount,
reason: command.reason
},
{
headers: {
'Authorization': `Bearer ${token}`,
'X-Service-Id': 'sport-tech-club'
}
}
);
return this.mapToTransfer(response.data);
}
}Integrações Planejadas
8. ANALYTICS PLATFORMS
Status: 🔴 Planejado
Providers: Google Analytics 4, Mixpanel, Amplitude
// domain/integrations/ports/analytics.port.ts
export interface AnalyticsPort {
trackEvent(event: AnalyticsEvent): Promise<void>;
identifyUser(userId: string, traits: UserTraits): Promise<void>;
trackPageView(page: PageView): Promise<void>;
}9. CRM INTEGRATION
Status: 🔴 Planejado
Providers: HubSpot, RD Station
// domain/integrations/ports/crm.port.ts
export interface CrmPort {
createContact(contact: Contact): Promise<string>;
updateContact(contactId: string, updates: Partial<Contact>): Promise<void>;
createDeal(deal: Deal): Promise<string>;
trackActivity(activity: Activity): Promise<void>;
}10. ERP INTEGRATION
Status: 🔴 Planejado
Purpose: Integração financeira e contábil
// domain/integrations/ports/erp.port.ts
export interface ErpPort {
syncInvoice(invoice: Invoice): Promise<void>;
syncPayment(payment: Payment): Promise<void>;
getAccountBalance(accountId: string): Promise<AccountBalance>;
}Padrões de Integração
Retry Policy
// infrastructure/integrations/common/retry-policy.ts
export class RetryPolicy {
constructor(private readonly config: RetryConfig) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === this.config.maxAttempts) {
throw lastError;
}
if (!this.isRetriable(error)) {
throw error;
}
const delay = this.calculateDelay(attempt);
await this.sleep(delay);
}
}
throw lastError!;
}
private calculateDelay(attempt: number): number {
switch (this.config.backoff) {
case 'exponential':
return this.config.initialDelay * Math.pow(2, attempt - 1);
case 'linear':
return this.config.initialDelay * attempt;
default:
return this.config.initialDelay;
}
}
private isRetriable(error: any): boolean {
if (error.statusCode >= 400 && error.statusCode < 500) {
return error.statusCode === 429;
}
return true;
}
}Dead Letter Queue
// infrastructure/integrations/common/dead-letter-queue.ts
export class DeadLetterQueue {
constructor(
private readonly repository: FailedIntegrationRepository,
private readonly alertService: AlertService
) {}
async enqueue(failure: IntegrationFailure): Promise<void> {
await this.repository.save({
id: crypto.randomUUID(),
integration: failure.integration,
operation: failure.operation,
payload: failure.payload,
error: failure.error,
attempts: failure.attempts,
failedAt: new Date(),
status: 'pending_retry'
});
if (failure.isCritical) {
await this.alertService.sendAlert({
severity: 'high',
title: `Integration Failure: ${failure.integration}`,
description: failure.error.message
});
}
}
}Idempotency
// infrastructure/integrations/common/idempotency.middleware.ts
export class IdempotencyMiddleware {
constructor(private readonly cache: CacheService) {}
async execute<T>(
key: string,
fn: () => Promise<T>,
ttl: number = 86400
): Promise<T> {
const cached = await this.cache.get<T>(key);
if (cached) {
return cached;
}
const result = await fn();
await this.cache.set(key, result, ttl);
return result;
}
}Segurança
API Authentication
// infrastructure/integrations/common/auth/service-auth.service.ts
export class ServiceAuthService {
private tokenCache = new Map<string, CachedToken>();
async getServiceToken(scope?: string): Promise<string> {
const cacheKey = scope || 'default';
const cached = this.tokenCache.get(cacheKey);
if (cached && !this.isTokenExpired(cached)) {
return cached.token;
}
const token = await this.keycloak.authenticateService(
this.config.clientId,
this.config.clientSecret
);
this.tokenCache.set(cacheKey, {
token: token.accessToken,
expiresAt: new Date(Date.now() + token.expiresIn * 1000)
});
return token.accessToken;
}
private isTokenExpired(cached: CachedToken): boolean {
return cached.expiresAt.getTime() - Date.now() < 300000;
}
}Request Signing
// infrastructure/integrations/common/request-signer.ts
import * as crypto from 'crypto';
export class RequestSigner {
constructor(private readonly secret: string) {}
sign(payload: any, timestamp: number = Date.now()): string {
const message = `${timestamp}.${JSON.stringify(payload)}`;
return crypto
.createHmac('sha256', this.secret)
.update(message)
.digest('hex');
}
verify(payload: any, signature: string, timestamp: number): boolean {
if (Date.now() - timestamp > 300000) {
return false;
}
const expectedSignature = this.sign(payload, timestamp);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
}Resiliência e Confiabilidade
Health Checks
// infrastructure/integrations/common/health-check.service.ts
export class IntegrationHealthCheckService {
constructor(
private readonly integrations: Map<string, HealthCheckable>
) {}
async checkAll(): Promise<HealthCheckResult[]> {
const results = await Promise.allSettled(
Array.from(this.integrations.entries()).map(async ([name, integration]) => {
const start = Date.now();
try {
await integration.healthCheck();
return {
name,
status: 'healthy',
latency: Date.now() - start
};
} catch (error) {
return {
name,
status: 'unhealthy',
error: error.message,
latency: Date.now() - start
};
}
})
);
return results.map(r => r.status === 'fulfilled' ? r.value : r.reason);
}
}Fallback Strategies
// application/services/notification-with-fallback.service.ts
export class NotificationWithFallbackService {
async sendWithFallback(command: SendNotificationCommand): Promise<void> {
const strategies: CommunicationChannel[] = [
command.channel,
...this.getFallbackChannels(command.channel)
];
for (const channel of strategies) {
try {
await this.notificationService.sendNotification({
...command,
channel
});
return;
} catch (error) {
console.warn(`Failed via ${channel}, trying next`, error);
}
}
throw new AllChannelsFailedError('All channels failed');
}
}Monitoramento
Integration Metrics
// infrastructure/integrations/common/metrics.service.ts
import { Histogram, Counter, Gauge } from 'prom-client';
export class IntegrationMetricsService {
private readonly requestDuration: Histogram;
private readonly requestTotal: Counter;
private readonly requestErrors: Counter;
constructor() {
this.requestDuration = new Histogram({
name: 'integration_request_duration_seconds',
help: 'Duration of integration requests',
labelNames: ['integration', 'operation', 'status']
});
this.requestTotal = new Counter({
name: 'integration_requests_total',
help: 'Total number of integration requests',
labelNames: ['integration', 'operation']
});
this.requestErrors = new Counter({
name: 'integration_errors_total',
help: 'Total number of integration errors',
labelNames: ['integration', 'operation', 'error_type']
});
}
recordRequest(
integration: string,
operation: string,
duration: number,
success: boolean
): void {
this.requestTotal.inc({ integration, operation });
this.requestDuration.observe(
{ integration, operation, status: success ? 'success' : 'error' },
duration / 1000
);
}
}Logging
// infrastructure/integrations/common/integration-logger.ts
export class IntegrationLogger {
constructor(
private readonly logger: Logger,
private readonly integration: string
) {}
logRequest(operation: string, payload: any): void {
this.logger.info('Integration request', {
integration: this.integration,
operation,
payload: this.sanitize(payload)
});
}
logError(operation: string, error: Error): void {
this.logger.error('Integration error', {
integration: this.integration,
operation,
error: {
message: error.message,
stack: error.stack
}
});
}
private sanitize(data: any): any {
const sensitiveKeys = ['password', 'apiKey', 'secret', 'token'];
if (typeof data !== 'object' || data === null) {
return data;
}
const sanitized = { ...data };
for (const key of Object.keys(sanitized)) {
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
sanitized[key] = '[REDACTED]';
}
}
return sanitized;
}
}Conclusão
Esta documentação fornece uma visão completa da arquitetura de integrações do Sport Tech Club, seguindo os princípios de:
✅ Clean Architecture - Separação de camadas e dependências invertidas
✅ Domain-Driven Design - Foco no domínio, com ACL protegendo o core
✅ Resiliência - Circuit breakers, retries, fallbacks
✅ Observabilidade - Logs, métricas, health checks
✅ Segurança - Autenticação, autorização, secrets management
✅ Extensibilidade - Fácil adicionar novas integrações via Port/Adapter
Próximos Passos
- Implementar integrações prioritárias (Ziggy, Keycloak, Payments)
- Configurar monitoramento e alertas
- Criar testes de integração end-to-end
- Documentar runbooks para troubleshooting
- Estabelecer SLAs com fornecedores externos
Mantido por: Time de Arquitetura - Sport Tech Club
Última Atualização: 2026-01-09