1. O sistema: Pedidos
Vamos montar o núcleo de um módulo de pedidos, reaproveitando as tabelas
Cliente, Pedido e
PedidoItem já usadas ao longo do módulo, e amarrando tudo com uma
única procedure de checkout que usa transação, TVP, tratamento de erro e retorno estruturado —
o "produto final" de tudo que este módulo cobriu.
| Capítulo | Técnica aplicada no projeto |
|---|---|
| 02–04 | CRUD completo de Cliente e Produto, com RETURN/OUTPUT |
| 05–06 | Transação + TRY/CATCH no checkout do pedido |
| 07 | Tabela temporária para validar estoque antes de confirmar |
| 08 | TVP para receber a lista de itens do carrinho |
| 10 | SQL dinâmico na listagem de pedidos com ordenação configurável |
| 11 | Trigger de auditoria quando o status do pedido muda |
| 12 | Nomenclatura, cabeçalho de documentação e CREATE OR ALTER |
| 13 | Chamada final via C# com Dapper |
2. Modelo de dados
CREATE TABLE dbo.Produto
(
Id INT IDENTITY(1,1) PRIMARY KEY,
Nome NVARCHAR(150) NOT NULL,
Preco DECIMAL(10,2) NOT NULL,
Estoque INT NOT NULL DEFAULT 0
);
CREATE TABLE dbo.Pedido
(
Id INT IDENTITY(1,1) PRIMARY KEY,
ClienteId INT NOT NULL REFERENCES dbo.Cliente(Id),
Status NVARCHAR(20) NOT NULL DEFAULT 'Pendente',
ValorTotal DECIMAL(12,2) NOT NULL DEFAULT 0,
DataPedido DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);
CREATE TABLE dbo.PedidoItem
(
Id INT IDENTITY(1,1) PRIMARY KEY,
PedidoId INT NOT NULL REFERENCES dbo.Pedido(Id),
ProdutoId INT NOT NULL REFERENCES dbo.Produto(Id),
Quantidade INT NOT NULL,
PrecoUnit DECIMAL(10,2) NOT NULL
);
CREATE TYPE dbo.ItemCarrinhoTableType AS TABLE
(
ProdutoId INT NOT NULL,
Quantidade INT NOT NULL
);
GO
3. A procedure central: checkout
Esta é a procedure que sintetiza o módulo inteiro: recebe o carrinho como TVP, valida estoque com
uma tabela temporária, roda tudo dentro de uma transação com XACT_ABORT,
e devolve o Id do pedido criado via OUTPUT.
-- =============================================
-- Procedure: usp_PedidoCheckout
-- Descrição: Cria um pedido a partir de um carrinho, validando
-- estoque e debitando os produtos, tudo atomicamente.
-- Parâmetros: @ClienteId - cliente que está finalizando a compra
-- @Itens - carrinho (ProdutoId, Quantidade)
-- @PedidoId - OUTPUT com o Id do pedido criado
-- Retorno: 0 = sucesso; lança erro se algum item não tiver estoque
-- Autor: Wellington Marunaka
-- =============================================
CREATE OR ALTER PROCEDURE dbo.usp_PedidoCheckout
@ClienteId INT,
@Itens dbo.ItemCarrinhoTableType READONLY,
@PedidoId INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
-- 1. valida estoque insuficiente ANTES de tocar em qualquer tabela
CREATE TABLE #Insuficientes (ProdutoId INT, Disponivel INT, Solicitado INT);
INSERT INTO #Insuficientes (ProdutoId, Disponivel, Solicitado)
SELECT p.Id, p.Estoque, i.Quantidade
FROM @Itens i
INNER JOIN dbo.Produto p ON p.Id = i.ProdutoId
WHERE p.Estoque < i.Quantidade;
IF EXISTS (SELECT 1 FROM #Insuficientes)
THROW 50010, 'Um ou mais produtos não têm estoque suficiente.', 1;
BEGIN TRANSACTION;
-- 2. cria o pedido
INSERT INTO dbo.Pedido (ClienteId, Status)
VALUES (@ClienteId, 'Confirmado');
SET @PedidoId = SCOPE_IDENTITY();
-- 3. copia os itens do carrinho, travando o preço atual
INSERT INTO dbo.PedidoItem (PedidoId, ProdutoId, Quantidade, PrecoUnit)
SELECT @PedidoId, i.ProdutoId, i.Quantidade, p.Preco
FROM @Itens i
INNER JOIN dbo.Produto p ON p.Id = i.ProdutoId;
-- 4. debita o estoque
UPDATE p
SET p.Estoque = p.Estoque - i.Quantidade
FROM dbo.Produto p
INNER JOIN @Itens i ON i.ProdutoId = p.Id;
-- 5. calcula e grava o valor total
UPDATE dbo.Pedido
SET ValorTotal = (
SELECT SUM(Quantidade * PrecoUnit)
FROM dbo.PedidoItem
WHERE PedidoId = @PedidoId
)
WHERE Id = @PedidoId;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
INSERT INTO dbo.ErrorLog (Numero, Mensagem, Procedure_, DataOcorrencia)
VALUES (ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_PROCEDURE(), SYSDATETIME());
THROW;
END CATCH
END;
GO
4. Trigger de auditoria de status
CREATE TABLE dbo.PedidoStatusHistorico
(
Id INT IDENTITY(1,1) PRIMARY KEY,
PedidoId INT NOT NULL,
StatusAntigo NVARCHAR(20),
StatusNovo NVARCHAR(20),
AlteradoEm DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);
GO
CREATE OR ALTER TRIGGER trg_Pedido_AuditarStatus
ON dbo.Pedido
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF NOT UPDATE(Status)
RETURN;
INSERT INTO dbo.PedidoStatusHistorico (PedidoId, StatusAntigo, StatusNovo)
SELECT d.Id, d.Status, i.Status
FROM deleted d
INNER JOIN inserted i ON i.Id = d.Id
WHERE d.Status <> i.Status;
END;
GO
5. Chamando o checkout a partir do C#
public async Task<int> FinalizarCompraAsync(int clienteId, List<ItemCarrinho> itens)
{
var tabela = new DataTable();
tabela.Columns.Add("ProdutoId", typeof(int));
tabela.Columns.Add("Quantidade", typeof(int));
foreach (var item in itens)
tabela.Rows.Add(item.ProdutoId, item.Quantidade);
var parametros = new DynamicParameters();
parametros.Add("@ClienteId", clienteId);
parametros.Add("@Itens", tabela.AsTableValuedParameter("dbo.ItemCarrinhoTableType"));
parametros.Add("@PedidoId", dbType: DbType.Int32, direction: ParameterDirection.Output);
await using var conexao = new SqlConnection(_connectionString);
try
{
await conexao.ExecuteAsync(
"dbo.usp_PedidoCheckout",
parametros,
commandType: CommandType.StoredProcedure);
return parametros.Get<int>("@PedidoId");
}
catch (SqlException ex) when (ex.Number == 50010)
{
throw new EstoqueInsuficienteException(ex.Message);
}
}
catch (SqlException ex) when (ex.Number == 50010) usa exception
filters do C# para capturar especificamente o erro de negócio lançado pelo
THROW 50010 da procedure, traduzindo-o para uma exceção própria do
domínio da aplicação — o número do erro (definido na procedure) vira o "contrato" entre o banco
e o C#.
6. O que praticar a partir daqui
Este projeto toca a maior parte do que o módulo cobriu, mas um sistema real cresce em outras direções: paginação nas listagens, cancelamento de pedido (com estorno de estoque), relatórios agregados por período. São todas extensões diretas dos padrões já vistos — o próximo salto real de conteúdo vem nos módulos seguintes, quando essas procedures passam a ser consumidas por uma API ASP.NET Core (Módulo 4) construída sobre uma arquitetura MVC (Módulo 3).