- Python
- Django
- HTML
- CSS
- Testes E2E
- CI/CD
Mercado Cesar
Aplicativo web de compras para atacarejo com produtos, estoque, pedidos e entrega em domicílio.
O Mercado Cesar é um aplicativo web de compras para uma loja de atacarejo, desenvolvido em equipe. O cliente navega pelos produtos, monta o carrinho, escolhe entre retirada na loja ou entrega em domicílio e repete pedidos antigos; o time administra produtos, armazéns e estoque. É um Django clássico com templates server-rendered (sem API REST), banco SQLite no desenvolvimento e PostgreSQL na produção, imagens no Cloudinary e deploy no Render.
Arquitetura e stack
- Django 5.2 com templates (
base.html,produtos.html,estoque.html,checkout.htmletc.) e JavaScript vanilla emstatic/js. - Banco: SQLite em desenvolvimento, PostgreSQL em produção (o
settings.pylêDATABASE_URLe troca o engine comdj_database_url). - Imagens:
CloudinaryFieldnos produtos; em produção o storage de mídia viraMediaCloudinaryStorage. - Deploy: Render, com
build.sh(instala,collectstatic,migrateeloaddata) e CI/CD no GitHub Actions.
O app Django é único (mercadocesar) e o views.py concentra quase toda a lógica, com os CRUDs administrativos.
Modelos
Os modelos principais:
-
Produto, comnome,codigo,descricao,categoria,preco_custo,preco,unidade_medidae imagem. As constraints garantem que os campos de texto não fiquem vazios e os preços não sejam negativos:class Meta: constraints = [ CheckConstraint(check=~Q(codigo=""), name='codigo_nao_vazio'), CheckConstraint(check=~Q(descricao=""), name='descricao_nao_vazia'), CheckConstraint(check=~Q(categoria=""), name='categoria_nao_vazia'), CheckConstraint(check=~Q(unidade_medida=""), name='unidade_medida_nao_vazia'), ] -
ArmazemeEstoque(um par produto + armazém comunique_together), com o helperEstoque.abaixo_estoque_minimo(30). -
CartaoCredito, ligado ao usuário, que guarda sóbandeira, os 4 últimos dígitos e a validade. -
Carrinho/ItemCarrinho(um carrinho ativo por usuário) ePedido/ItemPedido. OItemPedidoé uma cópia do produto no momento da compra, compreco_unitariocongelado para o histórico não mudar quando o preço atual muda. -
Loja, com prazo de retirada e flagativa.
Regras de negócio
Duas regras estão concentradas em validators.py e no fluxo de entrega:
- Cartão: validação de Luhn no número, CVV (3 ou 4 dígitos), validade não vencida e identificação da bandeira pelo número (Visa, Mastercard com o range 2221–2720, Amex, Elo, Hipercard, Diners). Só os últimos 4 dígitos vão para o banco.
- Entrega: o CEP precisa ser da região do Recife (inicia com 5 e prefixo 50–54), com frete fixo de R$ 15 e prazo de 2 a 3 dias úteis conforme o prefixo; fora da área, a entrega nem é oferecida e o fluxo manda para retirada na loja.
A baixa de estoque na finalização prioriza os armazéns com mais unidades:
estoques = Estoque.objects.filter(
produto=item_carrinho.produto,
quantidade__gt=0
).order_by('-quantidade')
for estoque in estoques:
if quantidade_restante <= 0:
break
if estoque.quantidade >= quantidade_restante:
estoque.quantidade -= quantidade_restante
quantidade_restante = 0
else:
quantidade_restante -= estoque.quantidade
estoque.quantidade = 0
estoque.save()
Views e permissões
A autenticação usa os fluxos padrão do Django (UserCreationForm no registro e o login do framework). As telas administrativas usam user_passes_test:
produtos,armazenseestoqueexigemis_staff;lojas(gerenciar/ativar-desativar) e a listagem geral de pedidos exigemis_superuser.
Os CRUDs administrativos são implementados como uma view única por recurso que despacha pela query string ?action=list|add|edit|delete, e as rotas antigas (produtos/adicionar/, produtos/editar/<id>/) só redirecionam para essa view:
@user_passes_test(lambda u: u.is_staff)
def gerenciar_produtos(request):
action = request.GET.get('action', 'list')
produto_id = request.GET.get('id')
if action == 'delete' and produto_id:
...
if action == 'edit' and produto_id:
...
if action == 'add':
...
produtos = Produto.objects.all().order_by('codigo')
...
Carrinho, checkout e pedidos
O carrinho é salvo no banco (um ativo por usuário). O checkout guarda os dados de entrega na sessão (request.session['pedido_temp']), mostra a revisão com os cartões salvos e só no finalizar_pedido cria o Pedido. Nessa finalização, além de validar posse do cartão e estoque suficiente, o código copia os itens do carrinho para o ItemPedido com o preço atual, dá baixa no estoque e desativa o carrinho.
A repetição de pedido antigo é um POST JSON no próprio checkout (o “compra rápida”): ele busca o pedido somente do usuário logado, desativa o carrinho atual, recria os itens num carrinho novo e responde via JsonResponse.
O usuário vê os próprios pedidos em pedidos_recentes (com filtros) e os gestores veem todos em visualizar_pedidos (com filtros por tipo e data).
Configuração e deploy
O settings.py usa python-decouple: SECRET_KEY, DEBUG, ALLOWED_HOSTS e credenciais do Cloudinary vêm de variáveis de ambiente. O banco alterna pelo DATABASE_URL:
if DATABASE_URL:
db_config = dj_database_url.parse(DATABASE_URL)
if 'postgres' in DATABASE_URL:
db_config['ENGINE'] = 'django.db.backends.postgresql'
DATABASES = {'default': db_config}
else:
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3'}}
No Render o build.sh roda collectstatic, migrate e, se existir, carrega a fixture admin_user.json. Por ser plano gratuito, a aplicação hiberna e demora para “acordar” no primeiro acesso.
Particularidades e pontos que revisaria
-
Lixo de desenvolvimento versionado. O repositório tem o
db.sqlite3(400 KB) e as pastas__pycache__com arquivos.pyccommitados. Isso deveria estar no.gitignore. -
Contrato estranho no
Produto.descricaotemunique=Trueedefault="", mas existe uma constraint que proíbe descrição vazia. Na prática não dá para dois produtos terem a mesma descrição (mesmo que sejam itens diferentes com nomes/códigos distintos), e odefault=""conflita com a constraint de não-vazio. -
Finalização de pedido sem transação atômica. O
finalizar_pedidofaz várias escritas (cria oPedido, copia os itens, baixa estoque de um ou mais armazéns, desativa o carrinho) sem@transaction.atomic. Uma falha no meio deixa o estado parcial, e como a baixa usaquantidade -=sem lock (F()), dois checkouts simultâneos podem vender além do estoque. -
Views-monstro e rotas redundantes.
views.pytem cerca de 45 KB, com CRUDs inteiros despachados por?action=e rotas “wrapper” que só fazemredirect('gerenciar_produtos?action=...'). Funciona, mas o arquivo ficou difícil de navegar e há duplicação entre as rotas antigas e a view unificada. -
Critérios de permissão divergentes. Produtos, armazéns e estoque exigem
is_staff, enquanto lojas e a listagem geral de pedidos exigemis_superuser. Vale padronizar o nível entre as telas administrativas. -
Fallbacks arriscados no settings.
SECRET_KEYtem um valor fixo de fallback,DEBUGéTruepor padrão e existe umprintsolto nosettings.pyque roda a cada inicialização. Em produção as env são setadas, mas subir sem elas deixa debug ligado e chave conhecida. -
Admin criado por fixture no deploy. O
build.shcarregamercadocesar/fixtures/admin_user.json, que cria um usuário admin com senha embutida no repositório; se ninguém trocar a senha depois do deploy, ela é pública.
Pontos de melhoria
- Colocar
db.sqlite3e__pycache__no.gitignoree remover do histórico. - Revisar o modelo
Produto: tirar ouniqueda descrição (ou odefault="") para o contrato fazer sentido. - Envolver a finalização do pedido em
@transaction.atomice usarF()/select_for_updatena baixa de estoque. - Quebrar o
views.pypor recurso (ou pelo menos usarModelForme class-based views) e remover as rotas wrapper. - Padronizar as permissões administrativas (staff vs superuser) e os fallbacks de
SECRET_KEY/DEBUG. - Substituir a fixture de admin por criação de superusuário via env no primeiro boot (e rotacionar a senha).
Gabriel Rocha