Pular para conteúdo

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:

@SetMetadata('roles', ['OWNER', 'ADMIN'])
@UseGuards(JwtAuthGuard, RolesGuard)

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:

Instalação