From 7084489c7ee43432a9f3ac87da348f5f0d0bccff Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Sun, 10 May 2026 22:19:13 -0300 Subject: [PATCH 01/11] feat: implement initial checkout processing logic with simulated payment API --- src/checkout.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/checkout.py diff --git a/src/checkout.py b/src/checkout.py new file mode 100644 index 0000000..4f9b52e --- /dev/null +++ b/src/checkout.py @@ -0,0 +1,96 @@ +# Este arquivo contém código propositalmente ruim para fins educacionais em uma aula de refatoração. +# MÁS PRÁTICAS APLICADAS: +# 1. Função única com múltiplas responsabilidades (validação, cálculo, pagamento). +# 2. Nomes de variáveis ruins e não descritivos (x1, val, p, temp, res). +# 3. Falta de tipagem de dados (sem type hints). +# 4. Manipulação de dicionários brutos em vez de modelos de dados (sem Pydantic). +# 5. Aninhamento profundo de condicionais (código espaguete). +# 6. "Números mágicos" espalhados pelo código (valores fixos sem explicação). +# 7. Mock de chamadas externas e validações com prints. +# 8. Falta de tratamento de erros robusto. + +# Simulação de uma biblioteca de requisições HTTP para não adicionar dependências reais. +class FakeResponse: + def __init__(self, status_code, json_data): + self.status_code = status_code + self._json_data = json_data + def json(self): + return self._json_data + +def fake_post(url, json): + """Simula uma chamada POST para uma API de pagamento.""" + print(f"--- Simulando POST para API de pagamento: {url} ---") + if json['valor_total'] > 0 and json['valor_total'] < 9999: + print("--- Pagamento APROVADO (simulado) ---") + return FakeResponse(200, {"status": "pagamento_aprovado", "transacao_id": "xyz123abc"}) + else: + print("--- Pagamento RECUSADO (simulado) ---") + return FakeResponse(400, {"status": "pagamento_recusado", "motivo": "valor_invalido"}) + + +def processar_tudo(cart_data, u_data): + """ + Função gigante e mal escrita para processar um checkout completo. + Recebe dados do carrinho e do usuário em formato de dicionário. + """ + print("Iniciando processamento de checkout...") + val = 0 + + if cart_data and 'items' in cart_data: + # Validação de estoque e cálculo de valor + for p in cart_data['items']: + print(f"Verificando estoque para o produto ID: {p['id']}...") + # Estoque mockado + estoque_disponivel = 10 + if p['qtd'] <= estoque_disponivel: + print(f"Estoque OK para {p['qtd']} unidades do produto {p['id']}.") + x1 = p['preco'] * p['qtd'] + val = val + x1 + else: + print(f"ERRO: Estoque insuficiente para o produto {p['id']}.") + return "Erro de estoque" + + # Cálculo de frete e descontos + if val > 0: + print(f"Valor parcial: {val}") + # Frete fixo + frete = 15.50 + val = val + frete + print(f"Valor com frete: {val}") + + # Lógica de desconto aninhada + if val > 200: + if u_data['vip']: + print("Aplicando desconto VIP de 15%") + val = val * 0.85 + else: + print("Aplicando desconto padrão de 5%") + val = val * 0.95 + + # Simulação de chamada para API de pagamento + print("Preparando para processar pagamento...") + dados_pagamento = { + "id_usuario": u_data['id'], + "valor_total": round(val, 2), + "info_cartao": "XXXX-XXXX-XXXX-1234" # Dados sensíveis hardcoded + } + + res = fake_post("https://api.pagamento.exemplo/processar", json=dados_pagamento) + + if res.status_code == 200: + temp = res.json() + if temp['status'] == 'pagamento_aprovado': + print(f"Checkout finalizado com sucesso! ID da transação: {temp['transacao_id']}") + return {"sucesso": True, "transacao": temp['transacao_id']} + else: + print("Ocorreu um problema com o pagamento.") + return {"sucesso": False, "erro": "problema_na_api_de_pagamento"} + else: + print("API de pagamento retornou um erro.") + return {"sucesso": False, "erro": "api_pagamento_offline"} + else: + print("Carrinho vazio, nenhum valor a processar.") + return "Carrinho vazio" + else: + print("Dados do carrinho estão vazios ou em formato inválido.") + return "Dados inválidos" From f4f13b304629e866efc42d67edc49d3b62ad776a Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Sun, 10 May 2026 22:32:17 -0300 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=20refatora=C3=A7=C3=A3o=20check?= =?UTF-8?q?our-chaos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/checkout.py | 203 ++++++++++++++++++++++++++---------------------- src/main.py | 100 ++++++++++++++++++++++-- src/models.py | 4 + 3 files changed, 209 insertions(+), 98 deletions(-) diff --git a/src/checkout.py b/src/checkout.py index 4f9b52e..a3de3cc 100644 --- a/src/checkout.py +++ b/src/checkout.py @@ -1,96 +1,113 @@ -# Este arquivo contém código propositalmente ruim para fins educacionais em uma aula de refatoração. -# MÁS PRÁTICAS APLICADAS: -# 1. Função única com múltiplas responsabilidades (validação, cálculo, pagamento). -# 2. Nomes de variáveis ruins e não descritivos (x1, val, p, temp, res). -# 3. Falta de tipagem de dados (sem type hints). -# 4. Manipulação de dicionários brutos em vez de modelos de dados (sem Pydantic). -# 5. Aninhamento profundo de condicionais (código espaguete). -# 6. "Números mágicos" espalhados pelo código (valores fixos sem explicação). -# 7. Mock de chamadas externas e validações com prints. -# 8. Falta de tratamento de erros robusto. - -# Simulação de uma biblioteca de requisições HTTP para não adicionar dependências reais. -class FakeResponse: - def __init__(self, status_code, json_data): - self.status_code = status_code - self._json_data = json_data - def json(self): - return self._json_data - -def fake_post(url, json): - """Simula uma chamada POST para uma API de pagamento.""" - print(f"--- Simulando POST para API de pagamento: {url} ---") - if json['valor_total'] > 0 and json['valor_total'] < 9999: - print("--- Pagamento APROVADO (simulado) ---") - return FakeResponse(200, {"status": "pagamento_aprovado", "transacao_id": "xyz123abc"}) - else: - print("--- Pagamento RECUSADO (simulado) ---") - return FakeResponse(400, {"status": "pagamento_recusado", "motivo": "valor_invalido"}) - - -def processar_tudo(cart_data, u_data): +# 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: + 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... ---") + for item in items: + # Lógica de estoque mockada + mock_stock = 10 + 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 + +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.") + +class PaymentService: + """Serviço que interage com a API de pagamento.""" + def __init__(self, payment_api: FakePaymentAPI): + self.payment_api = payment_api + + 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]: """ - Função gigante e mal escrita para processar um checkout completo. - Recebe dados do carrinho e do usuário em formato de dicionário. + Orquestra o processo de checkout utilizando injeção de dependência. """ - print("Iniciando processamento de checkout...") - val = 0 + print("--- Iniciando processo de checkout refatorado ---") - if cart_data and 'items' in cart_data: - # Validação de estoque e cálculo de valor - for p in cart_data['items']: - print(f"Verificando estoque para o produto ID: {p['id']}...") - # Estoque mockado - estoque_disponivel = 10 - if p['qtd'] <= estoque_disponivel: - print(f"Estoque OK para {p['qtd']} unidades do produto {p['id']}.") - x1 = p['preco'] * p['qtd'] - val = val + x1 - else: - print(f"ERRO: Estoque insuficiente para o produto {p['id']}.") - return "Erro de estoque" - - # Cálculo de frete e descontos - if val > 0: - print(f"Valor parcial: {val}") - # Frete fixo - frete = 15.50 - val = val + frete - print(f"Valor com frete: {val}") - - # Lógica de desconto aninhada - if val > 200: - if u_data['vip']: - print("Aplicando desconto VIP de 15%") - val = val * 0.85 - else: - print("Aplicando desconto padrão de 5%") - val = val * 0.95 - - # Simulação de chamada para API de pagamento - print("Preparando para processar pagamento...") - dados_pagamento = { - "id_usuario": u_data['id'], - "valor_total": round(val, 2), - "info_cartao": "XXXX-XXXX-XXXX-1234" # Dados sensíveis hardcoded - } - - res = fake_post("https://api.pagamento.exemplo/processar", json=dados_pagamento) - - if res.status_code == 200: - temp = res.json() - if temp['status'] == 'pagamento_aprovado': - print(f"Checkout finalizado com sucesso! ID da transação: {temp['transacao_id']}") - return {"sucesso": True, "transacao": temp['transacao_id']} - else: - print("Ocorreu um problema com o pagamento.") - return {"sucesso": False, "erro": "problema_na_api_de_pagamento"} - else: - print("API de pagamento retornou um erro.") - return {"sucesso": False, "erro": "api_pagamento_offline"} - else: - print("Carrinho vazio, nenhum valor a processar.") - return "Carrinho vazio" - else: - print("Dados do carrinho estão vazios ou em formato inválido.") - return "Dados inválidos" + # 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} + diff --git a/src/main.py b/src/main.py index 18abb38..bd745ae 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,97 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Depends, HTTPException +from typing import List, Dict, Any -app = FastAPI() +from .models import CartItem, UserData, Product +from .cart import ShoppingCart +from .checkout import ( + process_checkout, + StockValidator, + ShippingService, + DiscountService, + PaymentService, + FakePaymentAPI, +) -@app.get("/") -def read_root(): - return {"status": "ok"} +app = FastAPI( + title="TechShop API", + description="API para um e-commerce de tecnologia, com gerenciamento de carrinho e checkout.", + version="1.0.0", +) + +# --- 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 + +@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)): + """ + 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: + 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}") diff --git a/src/models.py b/src/models.py index 38b930b..1214a54 100644 --- a/src/models.py +++ b/src/models.py @@ -8,3 +8,7 @@ class Product(BaseModel): class CartItem(BaseModel): product: Product quantity: int + +class UserData(BaseModel): + id: int + is_vip: bool = False From 6f4ee33e3eef82b9dd702db120e2bd25a8c71883 Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Sun, 10 May 2026 22:34:27 -0300 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20add=20static=20frontend=20e=20int?= =?UTF-8?q?egra=C3=A7=C3=A3o=20com=20FastAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 5 +++ static/index.html | 108 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 static/index.html diff --git a/src/main.py b/src/main.py index bd745ae..144f00b 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI, Depends, HTTPException +from fastapi.staticfiles import StaticFiles from typing import List, Dict, Any from .models import CartItem, UserData, Product @@ -18,6 +19,9 @@ 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") + # --- 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] = { @@ -95,3 +99,4 @@ def perform_checkout(user_data: UserData, cart: ShoppingCart = Depends(get_cart) except Exception as e: # Captura genérica para outros erros inesperados raise HTTPException(status_code=500, detail=f"Ocorreu um erro inesperado: {e}") + diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..7a51006 --- /dev/null +++ b/static/index.html @@ -0,0 +1,108 @@ + + + + + + TechShop - Frontend Simples + + + + +

TechShop - Simulação de Frontend

+ +

Produtos

+ + + +

Carrinho

+ +
+

Clique em "Atualizar Carrinho" para ver os detalhes.

+
+ +

Checkout

+ +
+

Aguardando finalização da compra...

+
+ + + + + From fc99f578433477abcf34d19059082bb61cafea54 Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Mon, 11 May 2026 21:55:42 -0300 Subject: [PATCH 04/11] Refactor code structure for improved readability and maintainability --- .github/workflows/ci.yml | 25 ++++++ pyproject.toml | 1 + tests/test_cart.py | 171 +++++++++++++++++++++++++++++++++++++++ uv.lock | 156 ++++++++++++++++++++++++++++++++++- 4 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/test_cart.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d7cdca2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +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: + python-version: '3.11' + - name: Install uv + run: | + pip install uv + uv --version + - name: Install dependencies + run: uv sync + - name: Run tests with coverage + run: uv run pytest --cov=src --cov-fail-under=80 diff --git a/pyproject.toml b/pyproject.toml index 08724b4..48eb15a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "fastapi>=0.136.1", + "pandas>=3.0.3", "pydantic>=2.13.4", "uvicorn>=0.46.0", ] diff --git a/tests/test_cart.py b/tests/test_cart.py new file mode 100644 index 0000000..5588f8d --- /dev/null +++ b/tests/test_cart.py @@ -0,0 +1,171 @@ +import pytest +from src.models import Product +from src.cart import ShoppingCart + +# Arrange: Fixtures para produtos +@pytest.fixture +def product_1(): + """Retorna um produto de exemplo com preço 10.00.""" + return Product(id=1, name="Test Product 1", price=10.00, category="Test") + +@pytest.fixture +def product_2(): + """Retorna um produto de exemplo com preço 50.00.""" + return Product(id=2, name="Test Product 2", price=50.00, category="Test") + +@pytest.fixture +def product_expensive(): + """Retorna um produto caro para testar descontos.""" + return Product(id=3, name="Expensive Product", price=600.00, category="Test") + +@pytest.fixture +def product_very_expensive(): + """Retorna um produto muito caro para testar descontos.""" + return Product(id=4, name="Very Expensive Product", price=1100.00, category="Test") + + +# --- Testes para a classe ShoppingCart --- + +def test_add_new_item_to_cart(): + """ + Testa se um novo item é adicionado corretamente ao carrinho. + """ + # Arrange + cart = ShoppingCart() + product = Product(id=1, name="Test", price=10.0, category="Test") + quantity = 2 + + # Act + cart.add_item(product, quantity) + + # Assert + assert len(cart.items) == 1 + assert cart.items[0].product == product + assert cart.items[0].quantity == quantity + +def test_add_existing_item_to_cart(): + """ + Testa se a quantidade de um item existente é atualizada corretamente. + """ + # Arrange + cart = ShoppingCart() + product = Product(id=1, name="Test", price=10.0, category="Test") + cart.add_item(product, 2) + + # Act + cart.add_item(product, 3) + + # Assert + assert len(cart.items) == 1 + assert cart.items[0].quantity == 5 + +def test_remove_item_from_cart(product_1): + """ + Testa se um item é removido corretamente do carrinho. + """ + # Arrange + cart = ShoppingCart() + cart.add_item(product_1, 1) + + # Act + cart.remove_item(product_1.id) + + # Assert + assert len(cart.items) == 0 + +def test_calculate_total_empty_cart(): + """ + Testa o cálculo do total para um carrinho vazio (caso de borda). + """ + # Arrange + cart = ShoppingCart() + + # Act + total = cart.calculate_total() + + # Assert + assert total == 0 + +def test_calculate_total_with_items(product_1, product_2): + """ + Testa o cálculo do total para um carrinho com múltiplos itens. + """ + # Arrange + cart = ShoppingCart() + cart.add_item(product_1, 5) # 5 * 10.00 = 50.00 + cart.add_item(product_2, 2) # 2 * 50.00 = 100.00 + + # Act + total = cart.calculate_total() + + # Assert + assert total == 150.00 + +def test_discount_no_discount_below_threshold(product_1): + """ + Testa se nenhum desconto é aplicado para totais abaixo de 500. + """ + # Arrange + cart = ShoppingCart() + cart.add_item(product_1, 49) # Total = 490.00 + + # Act + total_with_discount = cart.calculate_total_with_discount() + + # Assert + assert total_with_discount == 490.00 + +def test_discount_exact_threshold_500(product_1): + """ + Testa o cálculo do desconto no limiar exato de 500 (caso de borda). + """ + # Arrange + cart = ShoppingCart() + cart.add_item(product_1, 50) # Total = 500.00 + + # Act + total_with_discount = cart.calculate_total_with_discount() + + # Assert + assert total_with_discount == 500.00 # Sem desconto + +def test_discount_10_percent_just_above_500(product_expensive): + """ + Testa o desconto de 10% para um total ligeiramente acima de 500. + """ + # Arrange + cart = ShoppingCart() + cart.add_item(product_expensive, 1) # Total = 600.00 + + # Act + total_with_discount = cart.calculate_total_with_discount() + + # Assert + assert total_with_discount == 600.00 * 0.90 + +def test_discount_20_percent_just_above_1000(product_very_expensive): + """ + Testa o desconto de 20% para um total ligeiramente acima de 1000. + """ + # Arrange + cart = ShoppingCart() + cart.add_item(product_very_expensive, 1) # Total = 1100.00 + + # Act + total_with_discount = cart.calculate_total_with_discount() + + # Assert + assert total_with_discount == 1100.00 * 0.80 + +def test_discount_empty_cart(product_1): + """ + Testa o cálculo de desconto para um carrinho vazio (caso de borda). + """ + # Arrange + cart = ShoppingCart() + + # Act + total_with_discount = cart.calculate_total_with_discount() + + # Assert + assert total_with_discount == 0 diff --git a/uv.lock b/uv.lock index 04122c2..0c9f60e 100644 --- a/uv.lock +++ b/uv.lock @@ -2,8 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] @@ -383,6 +390,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -392,6 +460,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -570,6 +690,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "ruff" version = "0.15.12" @@ -595,6 +727,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "starlette" version = "1.0.0" @@ -614,6 +755,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "fastapi" }, + { name = "pandas" }, { name = "pydantic" }, { name = "uvicorn" }, ] @@ -630,6 +772,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.136.1" }, + { name = "pandas", specifier = ">=3.0.3" }, { name = "pydantic", specifier = ">=2.13.4" }, { name = "uvicorn", specifier = ">=0.46.0" }, ] @@ -664,6 +807,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + [[package]] name = "uvicorn" version = "0.46.0" From 3c5490b694f0441721f92d5c7967d614e1b95b95 Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Mon, 11 May 2026 21:57:54 -0300 Subject: [PATCH 05/11] fix: set PYTHONPATH environment variable for test coverage --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7cdca2..acfac62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,5 @@ jobs: run: uv sync - name: Run tests with coverage run: uv run pytest --cov=src --cov-fail-under=80 + env: + PYTHONPATH: . From 38647eaea438fc449f957f536bd2b3e6b881b763 Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Tue, 12 May 2026 17:21:05 -0300 Subject: [PATCH 06/11] feat: add coverage configuration to pytest settings --- .coverage | Bin 0 -> 53248 bytes pyproject.toml | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 .coverage diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..e0b25c9399abc72ebe55d310617b12a73bec7200 GIT binary patch literal 53248 zcmeI)U2oe|7zc1WPLnxJ=b|x1rA$3HsI8V}i3l zv$hihGA0f28Mxq!z-QrdyFr>Xm;_w#JbuYb*GyAxrlo(YNgex~< zff5bZ>&QU7t1W7}u3Z#D(=?MF3-m~qEUl!I7xY^nS)a6OYNemwT(G{-@|mwR>+6NL ztc`p#|78A`{5kW+yqztYCE9@n0uX=z1ZGB{e=VOaEG_Bx?*+2jR)HsL%8Q;CE`7GO zd3{S<-~90EmWZ~AwVa@3V?%5T&)pGS<%x#fR>F4bc1;Gh(-c8VC8>M9s`ELEw$WLO z6PCjKyjHR6lq*n8Dx&Mz9qH|fTWYTuCWwy~sNEpiK!qr~>2M0M7H3}$&wGzt3e0$ByX_RTz`>YVVa=s_ z?gmk`p4&MUxT0-4VVI6@2e#`7wX15qK-CWnu*hi1LU^OmP+2!Qpqo+7_*f%4!^{Wd zjFZQi8&o;RO<0E+`OuU(th3YWREhkXUZ=~ALph=Y2kxYtlkx^`9PCTC%>ivWjCZU# z6LN0RC|A$1Z}-%o@rz=ZvlxcqxJAK1P!D)#K2v!An66Q;RJ;D|HobMK$X?)vuPfB} zl`=i$AG~oSTUc4qAKnf-3iDK|cHj?I%&|^0*gT=@@czmDhDXVGC*e`lZVR^&4jIlZ zqQ;@#JUV7hB^c7&~00;;VC+!UxAERh+9(Df4sY!D1?`@=r#n-=RsL zAI4afdCRcMAFI8MJF-o)Nmtq)4OyClqcecugmp=YW!3EkVgKcqVfW;7&#=Bx0mIjc zJ*8R?lhg42)b)LvPOE&{^3Nw*#cE6XJPY}&qUySBC7tNpKsm1XbWGb9VI7`7D_t0t z;qP%7%{!}MosyFaPxIGl&?V!GRyrN&)Z_CNpN6ASY3}F2$;XK%5y~i5X;At!ME8}M z6dLd487nEj)Fl%xwIl1%aOXxDDRHNzo+~ao8(o%x3ha&&oFuGF*s*q`FKV8mo$<>#N3#`VKd7rOeNKm3)NX%Jo!NV#>sz}KyRHTuB< z0SG_<0uX=z1Rwwb2tWV=5P-n!2^e}x&+_$u%KAgI{-h@?5P$##AOHafKmY;|fB*y_ z009WRmIC>dal%UeEadc}ZY(dwe*S00Izz00bZa z0SG_<0uX=z1RyXF$Qvj0Ba=xa#yuWP1m_An{r$B+LAWWbERCC=YQ=M-2#FNxsaN+7IfnwE?`rq0@A1Rwwb2tWV=5P$##AOHafXxhbe-_WPJaE`A3)0flz z`KbyRx&Ak%u0?$PpW00Kv*uJ`B-j7A{~v|}1Rwwb2tWV=5P$##AOHafKwzc?4BgNc z`1^m|`cI=DED(SI1Rwwb2tWV=5P$##AOHaf%%Ff_ESS;#{~v4C6YKE|hKP_L009U< z00Izz00bZa0SG_<0*6Z=lS-TB;@zDEJ*}H>r{25v!QGZ00bZa0SG_<0uX=z1RyZ$0{H%a)>|0yK>z{}fB*y_ Q009U<00Izz00cDce+%{fQ~&?~ literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 48eb15a..f0f5f38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,6 @@ dev = [ "pytest-cov>=7.1.0", "ruff>=0.15.12", ] + +[tool.pytest.ini_options] +pythonpath = "." From 1e5426963c7c85064257197b2b76f78ff26ed05c Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Tue, 12 May 2026 17:24:43 -0300 Subject: [PATCH 07/11] feat: add unit tests for checkout process and cart functionality --- .coverage | Bin 53248 -> 53248 bytes pyproject.toml | 3 +- tests/test_checkout.py | 152 +++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 107 +++++++++++++++++++++++++++++ uv.lock | 56 ++++++++++++--- 5 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 tests/test_checkout.py create mode 100644 tests/test_main.py diff --git a/.coverage b/.coverage index e0b25c9399abc72ebe55d310617b12a73bec7200..2923e05565e041d152d9aac882acf31db206f764 100644 GIT binary patch delta 125 zcmZozz}&Eac>_xW4-5Z(2L6xyxA~9r@82vau%6$DjfI7iQ=qC`h?#|vQ;p>vL%#%* z9gmp}^Y7>SB1}LrEoO<^$G-3W{r~UV-g=edWsH+Q_6q|Idd9&2jQ_xW2NVBG2L6xyFE=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] diff --git a/tests/test_checkout.py b/tests/test_checkout.py new file mode 100644 index 0000000..b7a265a --- /dev/null +++ b/tests/test_checkout.py @@ -0,0 +1,152 @@ +import pytest +from unittest.mock import MagicMock, patch +from src.checkout import ( + StockValidator, + ShippingService, + DiscountService, + FakePaymentAPI, + PaymentService, + process_checkout, +) +from src.models import Product, CartItem, UserData + +# Fixtures +@pytest.fixture +def sample_product(): + return Product(id=1, name="Sample Product", price=100.0, category="Electronics") + +@pytest.fixture +def sample_cart_item(sample_product): + return CartItem(product=sample_product, quantity=2) + +@pytest.fixture +def vip_user(): + return UserData(id=1, name="VIP User", email="vip@test.com", is_vip=True) + +@pytest.fixture +def regular_user(): + return UserData(id=2, name="Regular User", email="regular@test.com", is_vip=False) + +# Testes para StockValidator +def test_stock_validator_success(sample_cart_item): + validator = StockValidator() + assert validator.validate([sample_cart_item]) is True + +def test_stock_validator_insufficient_stock(sample_cart_item): + validator = StockValidator() + sample_cart_item.quantity = 11 # mock_stock is 10 + with pytest.raises(ValueError, match="Estoque insuficiente para Sample Product"): + validator.validate([sample_cart_item]) + +# Testes para ShippingService +def test_shipping_service_calculate(): + service = ShippingService() + assert service.calculate([]) == 15.50 + +# Testes para DiscountService +def test_discount_service_over_1000(regular_user): + service = DiscountService() + assert service.calculate(1200.0, regular_user) == 1200.0 * 0.80 + +def test_discount_service_over_500(regular_user): + service = DiscountService() + assert service.calculate(600.0, regular_user) == 600.0 * 0.90 + +def test_discount_service_vip_user(vip_user): + service = DiscountService() + assert service.calculate(400.0, vip_user) == 400.0 * 0.85 + +def test_discount_service_no_discount(regular_user): + service = DiscountService() + assert service.calculate(400.0, regular_user) == 400.0 + +# Testes para FakePaymentAPI e PaymentService +def test_fake_payment_api_charge_success(): + api = FakePaymentAPI() + result = api.charge(100.0, 1) + assert result["status"] == "pagamento_aprovado" + +def test_fake_payment_api_charge_failure(): + api = FakePaymentAPI() + with pytest.raises(ConnectionError): + api.charge(10000.0, 1) + +def test_payment_service_process(): + mock_api = MagicMock() + mock_api.charge.return_value = {"status": "success"} + service = PaymentService(payment_api=mock_api) + user = UserData(id=1, name="Test", email="test@test.com", is_vip=False) + result = service.process(user, 150.0) + mock_api.charge.assert_called_once_with(amount=150.0, user_id=1) + assert result == {"status": "success"} + +# Testes para o orquestrador process_checkout +def test_process_checkout_success(sample_cart_item, regular_user): + # Mocks para os serviços + stock_validator = MagicMock() + shipping_service = MagicMock() + discount_service = MagicMock() + payment_service = MagicMock() + + # Configuração dos retornos dos mocks + stock_validator.validate.return_value = True + shipping_service.calculate.return_value = 15.50 + # subtotal (200) + shipping (15.50) = 215.50 + discount_service.calculate.return_value = 215.50 # Sem desconto + payment_service.process.return_value = {"status": "pagamento_aprovado"} + + result = process_checkout( + cart_items=[sample_cart_item], + user_data=regular_user, + stock_validator=stock_validator, + shipping_service=shipping_service, + discount_service=discount_service, + payment_service=payment_service, + ) + + # Asserts + stock_validator.validate.assert_called_once() + shipping_service.calculate.assert_called_once() + discount_service.calculate.assert_called_once() + payment_service.process.assert_called_once() + assert result["success"] is True + assert result["payment_details"]["status"] == "pagamento_aprovado" + +def test_process_checkout_stock_failure(sample_cart_item, regular_user): + stock_validator = MagicMock() + stock_validator.validate.side_effect = ValueError("Estoque insuficiente") + + shipping_service = MagicMock() + discount_service = MagicMock() + payment_service = MagicMock() + + with pytest.raises(ValueError, match="Estoque insuficiente"): + process_checkout( + cart_items=[sample_cart_item], + user_data=regular_user, + stock_validator=stock_validator, + shipping_service=shipping_service, + discount_service=discount_service, + payment_service=payment_service, + ) + +def test_process_checkout_payment_failure(sample_cart_item, regular_user): + stock_validator = MagicMock() + shipping_service = MagicMock() + discount_service = MagicMock() + payment_service = MagicMock() + + stock_validator.validate.return_value = True + shipping_service.calculate.return_value = 15.50 + discount_service.calculate.return_value = 215.50 + payment_service.process.side_effect = ConnectionError("Falha no pagamento") + + with pytest.raises(ConnectionError, match="Falha no pagamento"): + process_checkout( + cart_items=[sample_cart_item], + user_data=regular_user, + stock_validator=stock_validator, + shipping_service=shipping_service, + discount_service=discount_service, + payment_service=payment_service, + ) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..d0912b2 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,107 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch, MagicMock +from src.main import app, get_cart +from src.models import Product, CartItem, UserData +from src.cart import ShoppingCart + +client = TestClient(app) + +# Fixtures +@pytest.fixture +def sample_product(): + return Product(id=1, name="Sample Product", price=100.0, category="Electronics") + +@pytest.fixture +def sample_cart_item(sample_product): + return CartItem(product=sample_product, quantity=1) + +@pytest.fixture +def mock_cart(): + cart = ShoppingCart() + # Sobrescreve a dependência do carrinho para usar nosso mock + app.dependency_overrides[get_cart] = lambda: cart + return cart + +# Testes para a rota /cart/add +def test_add_to_cart_success(mock_cart, sample_cart_item): + response = client.post("/cart/add", json=sample_cart_item.model_dump()) + assert response.status_code == 200 + assert len(mock_cart.items) == 1 + assert mock_cart.items[0].quantity == 1 + +# Testes para a rota /cart +def test_get_cart_details_success(mock_cart, sample_cart_item): + mock_cart.add_item(sample_cart_item.product, sample_cart_item.quantity) + response = client.get("/cart") + assert response.status_code == 200 + data = response.json() + assert data["subtotal"] == 100.0 + assert len(data["items"]) == 1 + +# Testes para a rota /checkout +def test_checkout_empty_cart(mock_cart): + response = client.post("/checkout", json=UserData(id=1, name="Test", email="a@a.com", is_vip=False).model_dump()) + assert response.status_code == 400 + assert "Carrinho está vazio" in response.json()["detail"] + +@patch('src.main.process_checkout') +def test_checkout_success(mock_process_checkout, mock_cart, sample_cart_item): + # Arrange + mock_cart.add_item(sample_cart_item.product, 1) + user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False) + + # Configura o mock para retornar um resultado de sucesso + mock_process_checkout.return_value = {"success": True, "payment_details": {"status": "pagamento_aprovado"}} + + # Act + response = client.post("/checkout", json=user_data.model_dump()) + + # Assert + assert response.status_code == 200 + assert response.json()["status"] == "Sucesso" + mock_process_checkout.assert_called_once() + # Verifica se o carrinho foi limpo + assert len(mock_cart.items) == 0 + +@patch('src.main.process_checkout') +def test_checkout_stock_failure(mock_process_checkout, mock_cart, sample_cart_item): + mock_cart.add_item(sample_cart_item.product, 1) + user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False) + + mock_process_checkout.side_effect = ValueError("Estoque insuficiente") + + response = client.post("/checkout", json=user_data.model_dump()) + + assert response.status_code == 400 + assert "Estoque insuficiente" in response.json()["detail"] + +@patch('src.main.process_checkout') +def test_checkout_payment_failure(mock_process_checkout, mock_cart, sample_cart_item): + mock_cart.add_item(sample_cart_item.product, 1) + user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False) + + mock_process_checkout.side_effect = ConnectionError("Falha no pagamento") + + response = client.post("/checkout", json=user_data.model_dump()) + + assert response.status_code == 503 + assert "Falha no pagamento" in response.json()["detail"] + +@patch('src.main.process_checkout') +def test_checkout_unexpected_error(mock_process_checkout, mock_cart, sample_cart_item): + mock_cart.add_item(sample_cart_item.product, 1) + user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False) + + mock_process_checkout.side_effect = Exception("Erro inesperado") + + response = client.post("/checkout", json=user_data.model_dump()) + + assert response.status_code == 500 + assert "Ocorreu um erro inesperado" in response.json()["detail"] + +# Limpa as substituições de dependência após os testes +@pytest.fixture(autouse=True, scope="module") +def cleanup(): + yield + app.dependency_overrides.clear() diff --git a/uv.lock b/uv.lock index 0c9f60e..daf7e08 100644 --- a/uv.lock +++ b/uv.lock @@ -82,6 +82,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, ] +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + [[package]] name = "click" version = "8.3.3" @@ -259,6 +268,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.14" @@ -760,8 +797,9 @@ dependencies = [ { name = "uvicorn" }, ] -[package.dev-dependencies] +[package.optional-dependencies] dev = [ + { name = "httpx" }, { name = "mypy" }, { name = "playwright" }, { name = "pytest" }, @@ -772,19 +810,17 @@ dev = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.136.1" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "pandas", specifier = ">=3.0.3" }, + { name = "playwright", marker = "extra == 'dev'", specifier = ">=1.59.0" }, { name = "pydantic", specifier = ">=2.13.4" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.12" }, { name = "uvicorn", specifier = ">=0.46.0" }, ] - -[package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=2.0.0" }, - { name = "playwright", specifier = ">=1.59.0" }, - { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-cov", specifier = ">=7.1.0" }, - { name = "ruff", specifier = ">=0.15.12" }, -] +provides-extras = ["dev"] [[package]] name = "typing-extensions" From dd3e97cae11ed3620663d578ecb3a261168c0e54 Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Tue, 12 May 2026 17:26:15 -0300 Subject: [PATCH 08/11] feat: update CI workflow to install development dependencies --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acfac62..50fa618 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: pip install uv uv --version - name: Install dependencies - run: uv sync + run: uv pip sync --with dev - name: Run tests with coverage run: uv run pytest --cov=src --cov-fail-under=80 env: From c3dff177481b53d760de36363ff6d4445ce35d76 Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Tue, 12 May 2026 17:27:22 -0300 Subject: [PATCH 09/11] feat: update CI workflow to install development dependencies correctly --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50fa618..08a545e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: pip install uv uv --version - name: Install dependencies - run: uv pip sync --with dev + run: uv pip install -e ".[dev]" - name: Run tests with coverage run: uv run pytest --cov=src --cov-fail-under=80 env: From e1323d787fcc0ddadb3dd60e964d11516af5cfdc Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Tue, 12 May 2026 17:28:57 -0300 Subject: [PATCH 10/11] feat: add step to create virtual environment in CI workflow --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08a545e..57a3b94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: 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 From ca12b5bb71137320606b5015de2638b52d671dc4 Mon Sep 17 00:00:00 2001 From: Axel Patrick Chepanski Gonzaga Date: Tue, 12 May 2026 20:00:34 -0300 Subject: [PATCH 11/11] feat: update backlog and add product requirements document for authentication and user profiles --- docs/BACKLOG.md | 38 ++++++++++++++++++++++++-------------- docs/PRD_AUTH.md | 14 ++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 docs/PRD_AUTH.md diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 5f7dabf..fa57b48 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -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)) diff --git a/docs/PRD_AUTH.md b/docs/PRD_AUTH.md new file mode 100644 index 0000000..80641bc --- /dev/null +++ b/docs/PRD_AUTH.md @@ -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.