Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
Comment thread
AxelPCG marked this conversation as resolved.
python-version: '3.11'
- name: Install uv
run: |
pip install uv
uv --version
- name: Create Virtual Environment
run: uv venv
- name: Install dependencies
run: uv pip install -e ".[dev]"
- name: Run tests with coverage
run: uv run pytest --cov=src --cov-fail-under=80
env:
PYTHONPATH: .
38 changes: 24 additions & 14 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
# Backlog da Sprint - Fluxo de Carrinho
# Backlog do Produto - TechShop

## User Stories
Este documento rastreia todas as User Stories planejadas para o projeto TechShop.

1. **US001 - Adicionar item ao carrinho:**
* **Como um** usuário,
* **Eu quero** adicionar um produto ao meu carrinho de compras a partir da página de detalhes do produto,
* **Para que** eu possa comprá-lo posteriormente.
## MVP - Carrinho e Checkout

2. **US002 - Visualizar total do carrinho:**
* **Como um** usuário,
* **Eu quero** ver o valor total dos itens no meu carrinho,
* **Para que** eu saiba o custo total da minha compra antes de ir para o checkout.
### Gerenciamento de Carrinho
- **US1:** Como cliente, quero poder adicionar produtos ao meu carrinho de compras para que eu possa comprá-los mais tarde. ([Issue #11](https://github.com/AxelPCG/techshop/issues/11))
- **US2:** Como cliente, quero poder remover produtos do meu carrinho de compras para ajustar meu pedido. ([Issue #12](https://github.com/AxelPCG/techshop/issues/12))
- **US3:** Como cliente, quero poder visualizar os produtos no meu carrinho para revisar meu pedido antes de finalizar a compra. ([Issue #13](https://github.com/AxelPCG/techshop/issues/13))

3. **US003 - Remover item do carrinho:**
* **Como um** usuário,
* **Eu quero** remover um item do meu carrinho de compras,
* **Para que** eu possa gerenciar os produtos que desejo comprar.
### Visualização de Produtos
- **US6:** Como cliente, quero poder navegar por uma lista de produtos para descobrir o que está disponível na loja. ([Issue #16](https://github.com/AxelPCG/techshop/issues/16))
- **US7:** Como cliente, quero poder ver os detalhes de um produto específico para tomar uma decisão de compra informada. ([Issue #17](https://github.com/AxelPCG/techshop/issues/17))

### Checkout
- **US4:** Como cliente, quero poder iniciar o processo de checkout a partir do meu carrinho de compras para finalizar minha compra. ([Issue #14](https://github.com/AxelPCG/techshop/issues/14))
- **US5:** Como cliente, quero poder inserir minhas informações de envio e pagamento durante o checkout para que o pedido possa ser processado e entregue. ([Issue #15](https://github.com/AxelPCG/techshop/issues/15))

## Próximos Passos - Autenticação e Perfis

### Autenticação
- **US8:** Como um novo usuário, quero poder me cadastrar na plataforma usando e-mail e senha para ter uma conta pessoal. ([Issue #7](https://github.com/AxelPCG/techshop/issues/7))
- **US9:** Como um usuário cadastrado, quero poder fazer login no sistema para acessar minha conta e histórico. ([Issue #8](https://github.com/AxelPCG/techshop/issues/8))
- **US10:** Como um usuário logado, quero poder fazer logout para proteger minha conta em dispositivos compartilhados. ([Issue #9](https://github.com/AxelPCG/techshop/issues/9))

### Perfil de Usuário
- **US11:** Como um usuário logado, quero ter uma página de perfil onde posso ver e editar minhas informações básicas (como nome e endereço de entrega). ([Issue #10](https://github.com/AxelPCG/techshop/issues/10))
14 changes: 14 additions & 0 deletions docs/PRD_AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### Product Requirements Document: Autenticação e Perfis de Usuário

#### 1. Objetivo
Implementar um sistema de autenticação seguro que permita aos usuários criar contas, fazer login/logout e gerenciar um perfil básico. Isso é fundamental para personalizar a experiência de compra e permitir o rastreamento de pedidos.

#### 2. Requisitos Funcionais
* **US8:** Como um novo usuário, quero poder me cadastrar na plataforma usando e-mail and senha para ter uma conta pessoal.
* **US9:** Como um usuário cadastrado, quero poder fazer login no sistema para acessar minha conta e histórico.
* **US10:** Como um usuário logado, quero poder fazer logout para proteger minha conta em dispositivos compartilhados.
* **US11:** Como um usuário logado, quero ter uma página de perfil onde posso ver e editar minhas informações básicas (como nome e endereço de entrega).

#### 3. Requisitos Não-Funcionais
* **Segurança:** Senhas devem ser armazenadas de forma segura (hashed). O sistema deve ter proteção contra ataques comuns (ex: brute-force).
* **Usabilidade:** O processo de login e cadastro deve ser simples e intuitivo.
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.136.1",
"pandas>=3.0.3",
"pydantic>=2.13.4",
"uvicorn>=0.46.0",
]

[dependency-groups]
[project.optional-dependencies]
dev = [
"mypy>=2.0.0",
"playwright>=1.59.0",
"pytest>=9.0.3",
"pytest-cov>=7.1.0",
"ruff>=0.15.12",
"httpx>=0.27.0",
]

[tool.pytest.ini_options]
pythonpath = "."
113 changes: 113 additions & 0 deletions src/checkout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# checkout.py - Refatorado

from typing import List, Dict, Any
from .models import CartItem, UserData

# --- Protocolos para Injeção de Dependência ---

class StockValidatorProtocol:
def validate(self, items: List[CartItem]) -> bool: ...

class ShippingServiceProtocol:
def calculate(self, items: List[CartItem]) -> float: ...

class DiscountServiceProtocol:
def calculate(self, total_value: float, user: UserData) -> float: ...

class PaymentServiceProtocol:
Comment on lines +3 to +17
Comment on lines +3 to +17
def process(self, user: UserData, final_amount: float) -> Dict[str, Any]: ...

# --- Implementações Concretas dos Serviços ---

class StockValidator:
"""Valida o estoque dos itens (implementação mock)."""
def validate(self, items: List[CartItem]) -> bool:
print("--- Validando estoque... ---")
Comment on lines +22 to +25
for item in items:
# Lógica de estoque mockada
mock_stock = 10
Comment on lines +24 to +28
if item.quantity > mock_stock:
print(f"ERRO: Estoque insuficiente para o produto {item.product.id}.")
raise ValueError(f"Estoque insuficiente para {item.product.name}")
print("--- Estoque OK. ---")
return True
Comment on lines +24 to +33

class ShippingService:
"""Calcula o frete (valor fixo para este exemplo)."""
_SHIPPING_FEE = 15.50

def calculate(self, items: List[CartItem]) -> float:
print(f"--- Calculando frete fixo de R$ {self._SHIPPING_FEE}... ---")
return self._SHIPPING_FEE

class DiscountService:
"""Calcula descontos com base no valor total e no tipo de usuário."""
def calculate(self, total_value: float, user: UserData) -> float:
print("--- Calculando descontos... ---")
if total_value > 1000:
print("--- Aplicando desconto de 20% (compras > R$ 1000) ---")
return total_value * 0.80
if total_value > 500:
print("--- Aplicando desconto de 10% (compras > R$ 500) ---")
return total_value * 0.90
if user.is_vip:
print("--- Aplicando desconto de 15% (usuário VIP) ---")
return total_value * 0.85
return total_value

class FakePaymentAPI:
"""Simula uma API de pagamento externa."""
def charge(self, amount: float, user_id: int) -> Dict[str, Any]:
print(f"--- Processando pagamento de R$ {amount:.2f} para o usuário {user_id}... ---")
if 0 < amount < 9999:
print("--- Pagamento APROVADO (simulado) ---")
return {"status": "pagamento_aprovado", "transaction_id": "xyz123abc"}
else:
print("--- Pagamento RECUSADO (simulado) ---")
raise ConnectionError("Falha ao processar pagamento: valor inválido.")

Comment on lines +60 to +68
class PaymentService:
"""Serviço que interage com a API de pagamento."""
def __init__(self, payment_api: FakePaymentAPI):
self.payment_api = payment_api
Comment on lines +69 to +72

def process(self, user: UserData, final_amount: float) -> Dict[str, Any]:
return self.payment_api.charge(amount=final_amount, user_id=user.id)

# --- Orquestrador do Checkout ---

def process_checkout(
cart_items: List[CartItem],
user_data: UserData,
stock_validator: StockValidatorProtocol,
shipping_service: ShippingServiceProtocol,
discount_service: DiscountServiceProtocol,
payment_service: PaymentServiceProtocol,
) -> Dict[str, Any]:
"""
Orquestra o processo de checkout utilizando injeção de dependência.
"""
print("--- Iniciando processo de checkout refatorado ---")

# 1. Validar estoque
stock_validator.validate(cart_items)

# 2. Calcular total inicial
subtotal = sum(item.product.price * item.quantity for item in cart_items)
print(f"Subtotal: R$ {subtotal:.2f}")

# 3. Calcular frete
shipping_cost = shipping_service.calculate(cart_items)
total_with_shipping = subtotal + shipping_cost
print(f"Total com frete: R$ {total_with_shipping:.2f}")

# 4. Aplicar descontos
final_amount = discount_service.calculate(total_with_shipping, user_data)
print(f"Valor final após descontos: R$ {final_amount:.2f}")

# 5. Processar pagamento
payment_result = payment_service.process(user_data, round(final_amount, 2))

print(f"--- Checkout finalizado! Resultado: {payment_result} ---")
return {"success": True, "payment_details": payment_result}

105 changes: 100 additions & 5 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,102 @@
from fastapi import FastAPI
from fastapi import FastAPI, Depends, HTTPException
from fastapi.staticfiles import StaticFiles
from typing import List, Dict, Any

app = FastAPI()
from .models import CartItem, UserData, Product
from .cart import ShoppingCart
Comment on lines +1 to +6
Comment on lines +3 to +6
from .checkout import (
process_checkout,
StockValidator,
ShippingService,
DiscountService,
PaymentService,
FakePaymentAPI,
)

app = FastAPI(
title="TechShop API",
description="API para um e-commerce de tecnologia, com gerenciamento de carrinho e checkout.",
version="1.0.0",
)

# Monta a pasta 'static' para servir arquivos estáticos (nosso frontend)
app.mount("/app", StaticFiles(directory="static", html=True), name="static")

Comment on lines +22 to +24
# --- Simulação de um "banco de dados" em memória para o carrinho ---
# Em uma aplicação real, isso seria substituído por um banco de dados (Redis, PostgreSQL, etc.)
shopping_cart_db: Dict[int, ShoppingCart] = {
1: ShoppingCart() # Carrinho para o usuário com ID 1
}

# --- Injeção de Dependências (FastAPI Depends) ---

def get_cart(user_id: int = 1) -> ShoppingCart:
"""Retorna a instância do carrinho de compras para o usuário."""
cart = shopping_cart_db.get(user_id)
if not cart:
raise HTTPException(status_code=404, detail="Carrinho não encontrado para o usuário.")
return cart

# Instâncias dos serviços que serão injetados
stock_validator = StockValidator()
shipping_service = ShippingService()
discount_service = DiscountService()
payment_api = FakePaymentAPI()
payment_service = PaymentService(payment_api=payment_api)


# --- Rotas da API ---

@app.post("/cart/add", response_model=CartItem, tags=["Carrinho"])
def add_to_cart(item: CartItem, cart: ShoppingCart = Depends(get_cart)):
"""
Adiciona um item ao carrinho de compras do usuário.

Se o produto já existir no carrinho, a quantidade será somada.
"""
cart.add_item(item.product, item.quantity)
return item

Comment on lines +57 to +59
@app.get("/cart", tags=["Carrinho"])
def get_cart_details(cart: ShoppingCart = Depends(get_cart)):
"""
Retorna os detalhes do carrinho de compras do usuário, incluindo
os itens, o subtotal e o total com descontos aplicados.
"""
return {
"items": cart.items,
"subtotal": cart.calculate_total(),
"total_with_discount": cart.calculate_total_with_discount(),
}

@app.post("/checkout", tags=["Checkout"])
def perform_checkout(user_data: UserData, cart: ShoppingCart = Depends(get_cart)):
"""
Comment on lines +50 to +74
Processa o checkout do carrinho de compras.

Realiza a validação de estoque, cálculo de frete, aplicação de descontos
e processamento do pagamento.
"""
if not cart.items:
raise HTTPException(status_code=400, detail="Carrinho está vazio.")

try:
Comment on lines +72 to +83
result = process_checkout(
cart_items=cart.items,
user_data=user_data,
stock_validator=stock_validator,
shipping_service=shipping_service,
discount_service=discount_service,
payment_service=payment_service,
)
# Limpa o carrinho após checkout bem-sucedido
cart.items.clear()
return {"status": "Sucesso", "details": result}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except ConnectionError as e:
raise HTTPException(status_code=503, detail=str(e))
except Exception as e:
# Captura genérica para outros erros inesperados
raise HTTPException(status_code=500, detail=f"Ocorreu um erro inesperado: {e}")
Comment on lines +99 to +101
Comment on lines +99 to +101

@app.get("/")
def read_root():
return {"status": "ok"}
4 changes: 4 additions & 0 deletions src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ class Product(BaseModel):
class CartItem(BaseModel):
product: Product
quantity: int

class UserData(BaseModel):
id: int
is_vip: bool = False
Loading
Loading