- Python
- Django
- HTML
- CSS
- E2E Tests
- CI/CD
Mercado Cesar
A web shopping application for a cash-and-carry store with products, inventory, orders and home delivery.
Mercado Cesar is a web shopping application for a cash-and-carry store, built as a team. Customers browse products, fill the cart, choose between in-store pickup or home delivery, and repeat past orders; the team manages products, warehouses and inventory. It is a classic Django app with server-rendered templates (no REST API), SQLite in development and PostgreSQL in production, images on Cloudinary, and deployment on Render.
Architecture and stack
- Django 5.2 with templates (
base.html,produtos.html,estoque.html,checkout.htmletc.) and vanilla JavaScript instatic/js. - Database: SQLite in development, PostgreSQL in production (
settings.pyreadsDATABASE_URLand switches the engine withdj_database_url). - Images:
CloudinaryFieldon products; in production the media storage becomesMediaCloudinaryStorage. - Deploy: Render, with
build.sh(install,collectstatic,migrateandloaddata) and CI/CD on GitHub Actions.
The Django app is a single one (mercadocesar) and views.py concentrates almost all the logic, along with the administrative CRUDs.
Models
The main models:
-
Produto, withnome,codigo,descricao,categoria,preco_custo,preco,unidade_medidaand an image. The constraints guarantee that text fields are not empty and that prices are not negative: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'), ] -
ArmazemandEstoque(a product + warehouse pair withunique_together), with the helperEstoque.abaixo_estoque_minimo(30). -
CartaoCredito, linked to the user, which stores onlybandeira, the last 4 digits and the expiry date. -
Carrinho/ItemCarrinho(one active cart per user) andPedido/ItemPedido.ItemPedidois a copy of the product at purchase time, withpreco_unitariofrozen so the history does not change when the current price changes. -
Loja, with a pickup deadline and anativaflag.
Business rules
Two rules are concentrated in validators.py and in the delivery flow:
- Card: Luhn validation on the number, CVV (3 or 4 digits), expiry date not passed, and brand detection from the number (Visa, Mastercard with the 2221–2720 range, Amex, Elo, Hipercard, Diners). Only the last 4 digits go to the database.
- Delivery: the ZIP code must belong to the Recife region (starts with 5 and prefix 50–54), with a flat shipping fee of R$ 15 and a deadline of 2 to 3 business days depending on the prefix; outside the area, delivery is not even offered and the flow redirects to in-store pickup.
Inventory decrement at checkout prioritizes the warehouses with the most units:
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 and permissions
Authentication uses Django’s standard flows (UserCreationForm on signup and the framework’s login). Administrative screens use user_passes_test:
produtos,armazensandestoquerequireis_staff;lojas(manage/activate-deactivate) and the general order listing requireis_superuser.
Administrative CRUDs are implemented as a single view per resource that dispatches on the query string ?action=list|add|edit|delete, and the old routes (produtos/adicionar/, produtos/editar/<id>/) only redirect to that 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')
...
Cart, checkout and orders
The cart is stored in the database (one active cart per user). Checkout keeps the delivery data in the session (request.session['pedido_temp']), shows the review with the saved cards, and only in finalizar_pedido creates the Pedido. In that finalization, besides validating card ownership and sufficient inventory, the code copies the cart items into ItemPedido with the current price, decrements inventory and deactivates the cart.
Repeating a past order is a JSON POST on checkout itself (the “quick purchase”): it looks up the order only for the logged-in user, deactivates the current cart, recreates the items in a new cart and responds via JsonResponse.
Users see their own orders in pedidos_recentes (with filters) and managers see all of them in visualizar_pedidos (with filters by type and date).
Configuration and deploy
settings.py uses python-decouple: SECRET_KEY, DEBUG, ALLOWED_HOSTS and the Cloudinary credentials come from environment variables. The database switches on 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'}}
On Render, build.sh runs collectstatic, migrate and, if it exists, loads the admin_user.json fixture. Because it is on the free plan, the application hibernates and takes a while to “wake up” on the first access.
Particularities and points I would revisit
-
Development junk under version control. The repository has
db.sqlite3(400 KB) and__pycache__folders with committed.pycfiles. This should be in.gitignore. -
Strange contract on
Produto.descricaohasunique=Trueanddefault="", but there is a constraint that forbids an empty description. In practice two products cannot have the same description (even if they are different items with distinct names/codes), anddefault=""conflicts with the non-empty constraint. -
Order finalization without an atomic transaction.
finalizar_pedidoperforms several writes (creates thePedido, copies the items, decrements inventory from one or more warehouses, deactivates the cart) without@transaction.atomic. A failure midway leaves the state partial, and since the decrement usesquantidade -=without a lock (F()), two simultaneous checkouts can oversell the inventory. -
Monster views and redundant routes.
views.pyis around 45 KB, with entire CRUDs dispatched by?action=and “wrapper” routes that only doredirect('gerenciar_produtos?action=...'). It works, but the file became hard to navigate and there is duplication between the old routes and the unified view. -
Divergent permission criteria. Products, warehouses and inventory require
is_staff, while stores and the general order listing requireis_superuser. It is worth standardizing the level across administrative screens. -
Risky fallbacks in settings.
SECRET_KEYhas a hardcoded fallback value,DEBUGisTrueby default and there is a strayprintinsettings.pythat runs on every startup. In production the env vars are set, but deploying without them leaves debug on and a known key. -
Admin created by fixture on deploy.
build.shloadsmercadocesar/fixtures/admin_user.json, which creates an admin user with a password embedded in the repository; if nobody changes the password after deploy, it is public.
Points for improvement
- Put
db.sqlite3and__pycache__in.gitignoreand remove them from history. - Revisit the
Produtomodel: drop theuniqueon the description (or thedefault="") so the contract makes sense. - Wrap order finalization in
@transaction.atomicand useF()/select_for_updateon the inventory decrement. - Break
views.pyup per resource (or at least useModelFormand class-based views) and remove the wrapper routes. - Standardize the administrative permissions (staff vs superuser) and the
SECRET_KEY/DEBUGfallbacks. - Replace the admin fixture with superuser creation via env on first boot (and rotate the password).
Gabriel Rocha