📚 Módulo 04: Backend - Serviços e Transações Assíncronas

Em Engenharia de Software, os controladores (routers) devem ser o mais simples possível, encarregando-se apenas da recepção e do retorno HTTP. O cérebro da nossa aplicação, contendo as regras e a orquestração do negócio, reside na Camada de Serviço (Service Layer).

Neste módulo, implementaremos as regras de negócio para faturamento de pedidos da TecLoja 02 de forma totalmente assíncrona, focando no princípio ACID de consistência transacional do estoque usando sessões do SQLAlchemy.


🏛️ 1. O Padrão Service com Sessão Assíncrona

Diferente do ecossistema Spring Boot, que gerencia dependências e transações por anotações implícitas (@Service, @Transactional), no ecossistema Python profissional nós fazemos o fluxo de dados explícito. Passamos a sessão de banco de dados (AsyncSession) no construtor de nosso serviço, o que simplifica drasticamente a escrita de testes unitários mockados.

Criaremos a estrutura de serviços no pacote app/services/.


📦 2. Serviço de Produtos

Este serviço encapsula todas as leituras e escritas no catálogo. As consultas de busca por categoria ou busca livre por nome usam o padrão SQL do SQLAlchemy 2.0.

Crie o arquivo em app/services/produto_service.py:

from typing import List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.produto import Categoria, Produto
from app.schemas.produto_schema import ProdutoFormSchema, ProdutoSchema
from app.schemas.mappers import Mapper
from app.exceptions import ResourceNotFoundException

class ProdutoService:
    def __init__(self, session: AsyncSession):
        self.session = session

    async def listar_todos(self) -> List[ProdutoSchema]:
        query = select(Produto)
        result = await self.session.execute(query)
        produtos = result.scalars().all()
        return [Mapper.produto_to_schema(p) for p in produtos]

    async def buscar_por_id(self, id: int) -> ProdutoSchema:
        p = await self.session.get(Produto, id)
        if not p:
            raise ResourceNotFoundException(f"Produto não encontrado com ID {id}")
        return Mapper.produto_to_schema(p)

    async def pesquisar_por_nome(self, busca: str) -> List[ProdutoSchema]:
        query = select(Produto).where(Produto.nome.ilike(f"%{busca}%"))
        result = await self.session.execute(query)
        produtos = result.scalars().all()
        return [Mapper.produto_to_schema(p) for p in produtos]

    async def criar(self, form: ProdutoFormSchema) -> ProdutoSchema:
        cat = await self.session.get(Categoria, form.categoria_id)
        if not cat:
            raise ResourceNotFoundException(f"Categoria {form.categoria_id} não encontrada.")
            
        p = Produto(
            nome=form.nome,
            descricao=form.descricao,
            preco=form.preco,
            estoque=form.estoque,
            categoria=cat
        )
        self.session.add(p)
        await self.session.flush() # Gera o ID do banco antes do commit
        return Mapper.produto_to_schema(p)

    async def atualizar(self, id: int, form: ProdutoFormSchema) -> ProdutoSchema:
        p = await self.session.get(Produto, id)
        if not p:
            raise ResourceNotFoundException(f"Produto {id} não encontrado.")
            
        cat = await self.session.get(Categoria, form.categoria_id)
        if not cat:
            raise ResourceNotFoundException(f"Categoria {form.categoria_id} não encontrada.")
            
        p.nome = form.nome
        p.descricao = form.descricao
        p.preco = form.preco
        p.estoque = form.estoque
        p.categoria = cat
        
        await self.session.flush()
        return Mapper.produto_to_schema(p)

    async def deletar(self, id: int) -> None:
        p = await self.session.get(Produto, id)
        if not p:
            raise ResourceNotFoundException(f"Produto {id} não encontrado.")
        await self.session.delete(p)
        await self.session.flush()

🛒 3. Serviço de Pedidos e Baixa Transacional de Estoque

O faturamento de pedidos (Checkout) é a regra de negócio central da nossa aplicação. O fluxo de validações deve garantir:

  1. Validade do Cliente.
  2. Existência física de todos os itens do carrinho na base de dados.
  3. Disponibilidade de Estoque: se o cliente solicitar 3 unidades e houver apenas 2 no estoque, toda a operação de venda deve ser imediatamente abortada.
  4. Cópia histórica do preço unitário do eletrônico no ato da compra.
  5. Gravação segura em cascata do Pedido e seus respectivos ItemPedido.

🧠 Como funciona a transação com async with session.begin()?

Em Engenharia de Software, a consistência dos dados é garantida por transações ACID (Atômicas, Consistentes, Isoladas e Duráveis).

Ao enveloparmos o método com async with self.session.begin():, abrimos um escopo transacional ativo com o Neon PostgreSQL. Se percorrermos o carrinho debitando o estoque de 3 produtos com sucesso e, no 4º produto, o estoque estiver esgotado, a exceção BusinessException será disparada.

O bloco de contexto capturará a exceção e executará automaticamente um Rollback total da transação no banco de dados. Os 3 produtos anteriores retornarão aos seus estoques originais e nenhuma linha de pedido será persistida fisicamente, prevenindo de forma absoluta estados inconsistentes.

sequenceDiagram
    participant Front as Vue 3 (Carrinho)
    participant API as FastAPI Router
    participant Service as PedidoService
    participant DB as PostgreSQL (Neon)

    Front->>API: POST /api/pedidos (DTO)
    API->>Service: realizar_pedido()
    Service->>DB: BEGIN TRANSACTION
    loop Para cada Item
        Service->>DB: Busca Produto
        alt Estoque Insuficiente
            Service-->>API: BusinessException
            Service->>DB: ROLLBACK TRANSACTION
            API-->>Front: 400 Bad Request
        else Estoque Ok
            Service->>Service: Debita Estoque
        end
    end
    Service->>DB: FLUSH / COMMIT TRANSACTION
    Service-->>API: PedidoSchema (DTO)
    API-->>Front: 201 Created

Crie o arquivo em app/services/pedido_service.py:

from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.pedido import Cliente, Pedido, ItemPedido
from app.models.produto import Produto
from app.schemas.pedido_schema import PedidoFormSchema, PedidoSchema
from app.schemas.mappers import Mapper
from app.exceptions import ResourceNotFoundException, BusinessException

class PedidoService:
    def __init__(self, session: AsyncSession):
        self.session = session

    async def realizar_pedido(self, form: PedidoFormSchema) -> PedidoSchema:
        # async with session.begin() inicia a transação ativa no banco.
        # Qualquer falha ou exceção dentro do escopo dispara ROLLBACK automático!
        async with self.session.begin():
            # 1. Validar existência do Cliente
            cliente = await self.session.get(Cliente, form.cliente_id)
            if not cliente:
                raise ResourceNotFoundException(f"Cliente {form.cliente_id} não cadastrado.")

            # 2. Instanciar novo Pedido
            pedido = Pedido(
                cliente=cliente,
                status="PAGO",
                data_pedido=datetime.utcnow(),
                itens=[]
            )
            self.session.add(pedido)

            # 3. Processar itens, validar estoque e debitar saldo físico
            for item_form in form.itens:
                produto = await self.session.get(Produto, item_form.produto_id)
                if not produto:
                    raise ResourceNotFoundException(f"Produto {item_form.produto_id} não encontrado no catálogo.")

                # Regra de Ouro: Checar estoque físico
                if produto.estoque < item_form.quantidade:
                    raise BusinessException(
                        f"Estoque insuficiente para o eletrônico '{produto.nome}'. "
                        f"Solicitado: {item_form.quantidade}, Disponível: {produto.estoque}."
                    )

                # Debitar estoque físico do produto
                produto.estoque -= item_form.quantidade

                # Instanciar registro de ItemPedido com cópia histórica de preço
                item_pedido = ItemPedido(
                    pedido=pedido,
                    produto=produto,
                    quantidade=item_form.quantidade,
                    preco_unitario=produto.preco # Copia preço ATUAL (histórico de venda)
                )
                pedido.itens.append(item_pedido)

            # Grava fisicamente na base e executa o commit implícito no encerramento do bloco 'begin'
            await self.session.flush()

        # Recarrega relações necessárias para o mapeamento DTO
        return Mapper.pedido_to_schema(pedido)

✅ Pré-Requisitos deste Módulo

Antes de avançar para as regras de negócio assíncronas, certifique-se de que:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Estrutura da Camada Service: Verifique se as classes de regras de negócio foram criadas nos caminhos físicos corretos:
    • tecloja-backend/app/services/produto_service.py
    • tecloja-backend/app/services/pedido_service.py
  2. Sintaxe Transacional: Confirme que o método realizar_pedido no serviço de pedidos utiliza o escopo async with self.session.begin(): para proteger o banco.

⚠️ Erros Comuns

Erro Causa Solução
sqlalchemy.exc.InterfaceError: conn.send_parameter() is not supported Tentativa de utilizar tipos de dados incompatíveis ou não mapeados na query (ex: passar objetos Pydantic em vez de valores puros nas cláusulas where). Certifique-se de extrair os valores primitivos dos Schemas (ex: form.categoria_id) antes de passá-los aos filtros e consultas do SQLAlchemy.
O banco de dados grava dados mesmo após o disparo de uma BusinessException O escopo transacional foi aberto incorretamente ou o rollback automático falhou por commits explícitos intermediários executados manualmente. Remova chamadas explícitas de await session.commit() de dentro do escopo async with session.begin():. A persistência automática e o commit são delegados inteiramente ao encerramento do escopo do bloco de contexto.

🏁 Conclusão

Sensacional! Toda a lógica de negócio transacional assíncrona está pronta, garantindo que o catálogo de produtos e as vendas operem de forma impecável, blindados contra anomalias e concorrência incoerente.

No Módulo 05, criaremos os roteadores REST do FastAPI, habilitaremos o controle de origens compartilhadas (CORS Middleware) para aceitar conexões vindas do Vue 3 hospedado na Netlify e adicionaremos segurança com autenticação stateless por tokens assinados de JWT.


Voltar para o Sumário