Arquitetura¶
Visão Geral¶
A integração segue uma arquitetura em camadas com separação clara de responsabilidades:
graph TB
subgraph "Camada de Apresentação"
A[NayaxController]
B[RestaurantController]
C[AdminController]
end
subgraph "Camada de Segurança"
D[NayaxAuthGuard]
E[JwtAuthGuard]
F[RolesGuard]
end
subgraph "Camada de Negócio"
G[NayaxService]
H[RestaurantService]
I[UserRestaurantService]
J[AdminService]
end
subgraph "Camada de Processamento"
K[JobsProcessor]
L[Mapper]
M[SaiposClient]
end
subgraph "Camada de Dados"
N[(PostgreSQL)]
O[Prisma ORM]
end
A --> D
D --> G
G --> O
O --> N
K --> O
K --> L
L --> M
B --> E
E --> H
H --> O Componentes Principais¶
1. Controllers (Camada de Apresentação)¶
NayaxController¶
- Responsabilidade: Receber webhooks do Nayax
- Rota:
POST /webhooks/nayax - Guarda:
NayaxAuthGuard
@Controller('webhooks/nayax')
@UseGuards(NayaxAuthGuard)
export class NayaxController {
@Post()
@HttpCode(200)
async receive(@Body() body: any, @Req() req: Request) {
const result = await this.nayaxService.ingestAndEnqueue(body, req.user);
return result;
}
}
RestaurantController¶
- Responsabilidade: CRUD de restaurantes
- Rota base:
/restaurants - Guarda:
JwtAuthGuard
AdminController¶
- Responsabilidade: Métricas e painel administrativo
- Rota base:
/admin - Guarda:
JwtAuthGuard+RolesGuard(ADMIN)
2. Guards (Camada de Segurança)¶
NayaxAuthGuard¶
Suporta 4 métodos de autenticação (em ordem de prioridade):
graph TD
A[Request] --> B{Bearer Token?}
B -->|Sim| C{É JWT?}
C -->|Sim| D[Valida JWT]
C -->|Não| E{É Token Nayax?}
E -->|Sim| F[Valida Token]
E -->|Não| G[Unauthorized]
B -->|Não| H{Basic Auth?}
H -->|Sim| I[Valida Credenciais]
H -->|Não| J{Credentials no Body?}
J -->|Sim| K[Valida Credenciais]
J -->|Não| L[Unauthorized]
D --> M[Authorized]
F --> M
I --> M
K --> M JwtAuthGuard¶
Valida tokens JWT para usuários autenticados:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
RolesGuard¶
Verifica permissões baseadas em roles:
3. Services (Camada de Negócio)¶
NayaxService¶
Responsabilidades: - Validar estrutura do evento - Salvar evento completo no banco - Criar job na outbox
async ingestAndEnqueue(body: any, actor: User | null) {
// 1. Validar
if (!body.transactionKey) throw new BadRequestException();
// 2. Salvar evento
const event = await this.prisma.nayaxEvent.create({...});
// 3. Criar job
const job = await this.prisma.outboxJob.create({
data: {
eventKey: body.transactionKey,
action: 'CREATE',
status: 'PENDING'
}
});
return { success: true, jobId: job.id };
}
RestaurantService¶
Responsabilidades: - CRUD de restaurantes - Controle de acesso - Estatísticas
UserRestaurantService¶
Responsabilidades: - Vínculo usuário-restaurante - Gestão de permissões - Transferência de ownership
4. JobsProcessor (Camada de Processamento)¶
Processador assíncrono com dois Cron jobs:
@Injectable()
export class JobsProcessor {
@Cron('*/20 * * * * *') // A cada 20 segundos
async tick() {
const jobs = await this.findPendingJobs();
for (const job of jobs) {
await this.processOne(job.id);
}
}
@Cron('*/20 * * * * *') // A cada 20 segundos
async reaper() {
await this.cleanStuckJobs();
}
}
Fluxo de Processamento¶
sequenceDiagram
participant T as Tick (Cron)
participant DB as Database
participant M as Mapper
participant S as Saipos
T->>DB: Busca jobs PENDING/FAILED
DB-->>T: Lista de jobs
loop Para cada job
T->>DB: Lock otimista (updateMany)
alt Lock obtido
T->>DB: Busca evento completo
T->>M: Transforma Nayax → Saipos
M-->>T: Body formatado
T->>S: POST /order
alt Sucesso
S-->>T: 200 OK
T->>DB: status = SENT
else Erro temporário
S-->>T: 5xx
T->>DB: status = FAILED, nextRunAt = now + backoff
else Erro permanente
S-->>T: 400/422
T->>DB: status = DLQ
end
else Lock falhou
T->>T: Skip (outro worker processando)
end
end 5. Mapper (Transformação de Dados)¶
Converte formato Nayax para Saipos:
export function mapNayaxToSaipos(ev: any, cod_store_saipos: string): SaiposOrderDto {
// 1. Gerar IDs únicos
let order_id = ev.transactionKey;
if (isTest) {
order_id = `${order_id}-${timestamp}`;
}
// 2. Mapear itens
const items = ev.items.map(mapItem);
// 3. Mapear pagamentos
const payment_types = ev.payments.map(mapPayment);
// 4. Calcular totais
const total_amount = calculateTotal(items, payment_types);
return { order_id, items, payment_types, total_amount, ... };
}
6. SaiposClient (Cliente HTTP)¶
Cliente HTTP para comunicação com API Saipos:
@Injectable()
export class SaiposClient {
async createOrder(config: SaiposConfig, body: SaiposOrderDto) {
const response = await axios.post(`${config.baseUrl}/order`, body, {
headers: {
'id-partner': config.idPartner,
'secret': config.secret
}
});
return response.data;
}
async cancelOrder(config: SaiposConfig, orderId: string, cod_store: string) {
// implementação
}
}
7. Prisma ORM (Camada de Dados)¶
Interface type-safe com PostgreSQL:
model NayaxEvent {
transactionKey String @id
storeCode String
totalAmount Float
items Item[]
payments Payment[]
restaurant Restaurant? @relation(fields: [restaurantId], references: [id])
restaurantId String?
}
model OutboxJob {
id String @id @default(uuid())
eventKey String
action String // CREATE | CANCEL
status String // PENDING | PROCESSING | SENT | FAILED | DLQ
attempts Int @default(0)
nextRunAt DateTime?
processingAt DateTime?
lockedBy String?
lastError String?
}
Padrões de Design¶
1. Outbox Pattern¶
Garante processamento confiável de eventos:
graph LR
A[Webhook] --> B[Transação DB]
B --> C[Salva Evento]
B --> D[Cria Job]
D --> E[Outbox Table]
E --> F[Worker]
F --> G[API Externa] Benefícios: - Garantia de processamento (at-least-once) - Desacoplamento (webhook retorna imediatamente) - Retry automático - Observabilidade (todos os eventos no banco)
2. Repository Pattern¶
Isolamento da camada de dados via Prisma:
// Não acessamos o banco diretamente nos controllers
// ❌ BAD
@Controller()
export class Controller {
constructor(private prisma: PrismaService) {}
@Get()
async get() {
return this.prisma.restaurant.findMany();
}
}
// GOOD
@Controller()
export class Controller {
constructor(private service: RestaurantService) {}
@Get()
async get() {
return this.service.findAll();
}
}
3. Guard Pattern¶
Separação de responsabilidades de autenticação:
// Guards focam apenas em autenticação/autorização
@Injectable()
export class NayaxAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// Lógica de autenticação
return true;
}
}
// Services focam na lógica de negócio
@Injectable()
export class NayaxService {
ingestAndEnqueue(body: any) {
// Lógica de negócio
}
}
4. Strategy Pattern¶
Múltiplos métodos de autenticação:
// Cada método é uma "estratégia"
if (isBearerToken) {
return this.validateBearerToken(token);
} else if (isBasicAuth) {
return this.validateBasicAuth(credentials);
} else if (isBodyCredentials) {
return this.validateBodyCredentials(body);
}
Escalabilidade¶
Horizontal¶
O sistema pode ser escalado horizontalmente:
- Múltiplas instâncias: Lock otimista evita duplicação
- Stateless: Sem estado em memória
- Load balancer: Distribui webhooks entre instâncias
graph TB
LB[Load Balancer]
LB --> I1[Instance 1]
LB --> I2[Instance 2]
LB --> I3[Instance 3]
I1 --> DB[(Database)]
I2 --> DB
I3 --> DB
I1 --> W1[Worker 1]
I2 --> W2[Worker 2]
I3 --> W3[Worker 3] Vertical¶
Recursos podem ser aumentados:
- CPU: Mais workers simultâneos
- RAM: Cache de queries
- Storage: Mais eventos históricos
Monitoramento¶
Logs Estruturados¶
this.logger.log('Job processed', {
jobId: job.id,
eventKey: job.eventKey,
status: 'SENT',
duration: Date.now() - start
});
Métricas¶
- Taxa de sucesso/falha
- Tempo de processamento
- Queue depth (jobs pendentes)
- Retry rate
Alertas¶
- Jobs em DLQ
- Taxa de erro > 5%
- Queue depth > 100
- Tempo de processamento > 5min
Próximos Passos¶
Entendeu a arquitetura? Vamos para a instalação: