Skip to content

Commit 937c105

Browse files
committed
configure settings, add extra models, fix: models, create services, cruds, api endpoints
1 parent 4403b7e commit 937c105

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1941
-310
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies = [
3333
"python-multipart>=0.0.20",
3434
"ruff>=0.14.7",
3535
"sentry-sdk>=2.47.0",
36+
"stripe>=14.0.1",
3637
"types-python-jose>=3.5.0.20250531",
3738
"uvicorn[standard]>=0.38.0",
3839
]

src/api/routers.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
from fastapi import APIRouter
22

3-
from api.v1 import admin, auth, brand, category, dashboard, social_auth, users # noqa
3+
from api.v1 import (
4+
admin,
5+
auth,
6+
brand,
7+
cart,
8+
category,
9+
dashboard,
10+
orders,
11+
payments,
12+
social_auth,
13+
upload,
14+
users,
15+
)
16+
from api.v1.products import images, products, variants
417
from core.config import settings
518

619
api_router = APIRouter(
720
prefix=f"{settings.API_V1_PREFIX}",
821
)
922

23+
24+
api_router.include_router(upload.router, prefix="/Upload", tags=["Upload"])
1025
api_router.include_router(auth.router, prefix="/auth", tags=["Auth"])
1126
api_router.include_router(
1227
social_auth.router, prefix="/social_auth", tags=["Social Auth"]
@@ -16,5 +31,11 @@
1631
api_router.include_router(admin.router, prefix="/admin", tags=["Admin"])
1732
api_router.include_router(category.router, prefix="/categories", tags=["Categories"])
1833
api_router.include_router(brand.router, prefix="/brands", tags=["Brands"])
34+
api_router.include_router(cart.router, prefix="/carts", tags=["Carts"])
35+
api_router.include_router(products.router, prefix="/products", tags=["Products"])
36+
api_router.include_router(variants.router, prefix="/products", tags=["Variants"])
37+
api_router.include_router(images.router, prefix="/products", tags=["Images"])
38+
api_router.include_router(orders.router, prefix="/orders", tags=["Orders"])
39+
api_router.include_router(payments.router, prefix="/payments", tags=["Payments"])
1940

2041
__all__ = ("api_router",)

src/api/v1/brand.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter, Depends, HTTPException, status
1+
from fastapi import APIRouter, Depends, status
22

33
from db.crud.brand import BrandCRUD
44
from db.dependencies.auth import ADMIN_ROLE, SELLER_ROLE, require_roles
@@ -7,6 +7,7 @@
77
BrandOutScheme,
88
BrandUpdateScheme,
99
)
10+
from utils.shortcuts import get_or_404
1011

1112
router = APIRouter(prefix="/brands", tags=["Brands"])
1213

@@ -18,11 +19,7 @@ async def get_brands(brands_crud: BrandCRUD = Depends(BrandCRUD)):
1819

1920
@router.get("/{slug}", response_model=BrandOutScheme)
2021
async def get_brand(slug: str, brands_crud: BrandCRUD = Depends(BrandCRUD)):
21-
brand = await brands_crud.get_by_slug(slug)
22-
if not brand:
23-
raise HTTPException(
24-
status_code=status.HTTP_404_NOT_FOUND, detail="Brand not found"
25-
)
22+
brand = await get_or_404(brands_crud.get_by_slug(slug), "Brand not found")
2623
return brand
2724

2825

@@ -46,11 +43,7 @@ async def create_brand(
4643
async def update_brand(
4744
slug: str, data: BrandUpdateScheme, brands_crud: BrandCRUD = Depends(BrandCRUD)
4845
):
49-
brand = await brands_crud.get_by_slug(slug)
50-
if not brand:
51-
raise HTTPException(
52-
status_code=status.HTTP_404_NOT_FOUND, detail="Brand not found"
53-
)
46+
brand = await get_or_404(brands_crud.get_by_slug(slug), "Brand not found")
5447
return await brands_crud.update(brand, data)
5548

5649

@@ -60,9 +53,5 @@ async def update_brand(
6053
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
6154
)
6255
async def delete_brand(slug: str, brands_crud: BrandCRUD = Depends(BrandCRUD)):
63-
brand = await brands_crud.get_by_slug(slug)
64-
if not brand:
65-
raise HTTPException(
66-
status_code=status.HTTP_404_NOT_FOUND, detail="Brand not found"
67-
)
56+
brand = await get_or_404(brands_crud.get_by_slug(slug), "Brand not found")
6857
await brands_crud.delete(brand)

src/api/v1/cart.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# src/api/endpoints/cart.py
2+
from fastapi import APIRouter, Depends
3+
from sqlalchemy.ext.asyncio import AsyncSession
4+
5+
from db.dependencies.auth import get_current_user # твоя функция
6+
from db.dependencies.sessions import get_db_session
7+
from db.models.users import User
8+
from schemas.cart import (
9+
AddToCartScheme,
10+
CartResponse,
11+
UpdateQuantityScheme,
12+
)
13+
from services.cart_service import CartService
14+
15+
router = APIRouter(prefix="/carts", tags=["Carts"])
16+
17+
18+
def get_cart_service(session: AsyncSession = Depends(get_db_session)) -> CartService:
19+
return CartService(session)
20+
21+
22+
@router.get("/", response_model=CartResponse)
23+
async def get_cart(
24+
current_user: User = Depends(get_current_user),
25+
cart_service: CartService = Depends(get_cart_service),
26+
):
27+
cart = await cart_service.get_user_cart(current_user.id)
28+
return cart
29+
30+
31+
@router.post("/add", response_model=CartResponse)
32+
async def add_to_cart(
33+
data: AddToCartScheme,
34+
current_user: User = Depends(get_current_user),
35+
cart_service: CartService = Depends(get_cart_service),
36+
):
37+
await cart_service.add_to_cart(current_user.id, data.variant_id, data.quantity)
38+
return await cart_service.get_user_cart(current_user.id)
39+
40+
41+
@router.post("/update", response_model=CartResponse)
42+
async def update_quantity(
43+
data: UpdateQuantityScheme,
44+
current_user: User = Depends(get_current_user),
45+
cart_service: CartService = Depends(get_cart_service),
46+
):
47+
await cart_service.update_quantity(current_user.id, data.variant_id, data.quantity)
48+
return await cart_service.get_user_cart(current_user.id)
49+
50+
51+
@router.delete("/{variant_id}")
52+
async def remove_item(
53+
variant_id: int,
54+
current_user: User = Depends(get_current_user),
55+
cart_service: CartService = Depends(get_cart_service),
56+
):
57+
await cart_service.remove_from_cart(current_user.id, variant_id)
58+
return {"detail": "Item removed"}
59+
60+
61+
@router.delete("/", status_code=200)
62+
async def clear_cart(
63+
current_user: User = Depends(get_current_user),
64+
cart_service: CartService = Depends(get_cart_service),
65+
):
66+
await cart_service.clear_cart(current_user.id)
67+
return {"detail": "Cart cleared"}

src/api/v1/category.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter, Depends, HTTPException, status
1+
from fastapi import APIRouter, Depends, status
22

33
from db.crud.category import CategoryCRUD
44
from db.dependencies.auth import ADMIN_ROLE, SELLER_ROLE, require_roles
@@ -7,6 +7,7 @@
77
CategoryOutScheme,
88
CategoryUpdateScheme,
99
)
10+
from utils.shortcuts import get_or_404
1011

1112
router = APIRouter(prefix="/categories", tags=["Categories"])
1213

@@ -20,9 +21,9 @@ async def get_categories(categoryies_crud: CategoryCRUD = Depends(CategoryCRUD))
2021
async def get_category(
2122
slug: str, categoryies_crud: CategoryCRUD = Depends(CategoryCRUD)
2223
):
23-
category = await categoryies_crud.get_by_slug(slug)
24-
if not category:
25-
raise HTTPException(status_code=404, detail="Category not found")
24+
category = await get_or_404(
25+
categoryies_crud.get_by_slug(slug), "Category not found"
26+
)
2627
return category
2728

2829

@@ -48,10 +49,9 @@ async def update_category(
4849
data: CategoryUpdateScheme,
4950
categoryies_crud: CategoryCRUD = Depends(CategoryCRUD),
5051
):
51-
category = await categoryies_crud.get_by_slug(slug)
52-
if not category:
53-
raise HTTPException(status_code=404, detail="Category not found")
54-
52+
category = await get_or_404(
53+
categoryies_crud.get_by_slug(slug), "Category not found"
54+
)
5555
return await categoryies_crud.update(category, data)
5656

5757

@@ -63,8 +63,7 @@ async def update_category(
6363
async def delete_category(
6464
slug: str, categoryies_crud: CategoryCRUD = Depends(CategoryCRUD)
6565
):
66-
category = await categoryies_crud.get_by_slug(slug)
67-
if not category:
68-
raise HTTPException(status_code=404, detail="Category not found")
69-
66+
category = await get_or_404(
67+
categoryies_crud.get_by_slug(slug), "Category not found"
68+
)
7069
await categoryies_crud.delete(category)

src/api/v1/orders.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from fastapi import APIRouter, Depends, status
2+
from sqlalchemy.ext.asyncio import AsyncSession
3+
4+
from db.dependencies.sessions import get_db_session
5+
from schemas.order import CheckoutRequestScheme, OrderOutScheme
6+
from services.order_service import OrderService
7+
from utils.shortcuts import get_or_404
8+
9+
router = APIRouter(prefix="/orders", tags=["Orders"])
10+
11+
12+
async def get_order_service(
13+
session: AsyncSession = Depends(get_db_session),
14+
) -> OrderService:
15+
return OrderService(session)
16+
17+
18+
@router.post(
19+
"/checkout", response_model=OrderOutScheme, status_code=status.HTTP_201_CREATED
20+
)
21+
async def checkout(
22+
payload: CheckoutRequestScheme, service: OrderService = Depends(get_order_service)
23+
):
24+
order = await service.checkout(
25+
user_id=payload.user_id,
26+
address_id=payload.address_id,
27+
delivery_id=payload.delivery_id,
28+
items=payload.items,
29+
promo_code=payload.promo_code,
30+
currency=payload.currency,
31+
)
32+
return order
33+
34+
35+
@router.post("/{order_id}/pay", status_code=status.HTTP_200_OK)
36+
async def pay_success(
37+
order_id: int, service: OrderService = Depends(get_order_service)
38+
):
39+
order = await service.on_payment_success(order_id)
40+
return {"detail": "Payment confirmed", "order_id": order.id}
41+
42+
43+
@router.post("/{order_id}/cancel", status_code=status.HTTP_200_OK)
44+
async def cancel(order_id: int, service: OrderService = Depends(get_order_service)):
45+
order = await service.cancel_order(order_id)
46+
return {"detail": "Order cancelled", "order_id": order.id}
47+
48+
49+
@router.get("/{order_id}", response_model=OrderOutScheme)
50+
async def get_order(order_id: int, service: OrderService = Depends(get_order_service)):
51+
order = await service.crud.get_order(order_id)
52+
order = await get_or_404(order, "Order not found")
53+
return order

src/api/v1/payments.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# src/api/v1/payments.py
2+
import stripe
3+
from fastapi import APIRouter, Depends, Header, HTTPException, Request
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from core.config import settings
7+
from db.dependencies.sessions import get_db_session
8+
from services.payment_service import PaymentService
9+
10+
router = APIRouter(prefix="/payments", tags=["Payments"])
11+
12+
13+
async def get_payment_service(session: AsyncSession = Depends(get_db_session)):
14+
return PaymentService(session)
15+
16+
17+
# 1) create payment intent for order (returns client_secret)
18+
@router.post("/{order_id}/create")
19+
async def create_payment(
20+
order_id: int, service: PaymentService = Depends(get_payment_service)
21+
):
22+
return await service.create_payment_intent_for_order(order_id)
23+
24+
25+
# 2) webhook endpoint — Stripe will POST events here
26+
# IMPORTANT: do NOT use regular body parsing for verification — read raw body bytes
27+
@router.post("/webhook", status_code=200)
28+
async def stripe_webhook(
29+
request: Request,
30+
stripe_signature: str | None = Header(None),
31+
service: PaymentService = Depends(get_payment_service),
32+
):
33+
payload = await request.body()
34+
sig_header = stripe_signature or request.headers.get(
35+
"stripe-signature"
36+
) # header name lowercase may occur
37+
if sig_header is None:
38+
raise HTTPException(status_code=400, detail="Missing stripe signature")
39+
40+
try:
41+
event = stripe.Webhook.construct_event(
42+
payload=payload,
43+
sig_header=sig_header,
44+
secret=settings.STRIPE_WEBHOOK_SECRET,
45+
)
46+
except stripe.error.SignatureVerificationError:
47+
raise HTTPException(status_code=400, detail="Invalid signature")
48+
except Exception as e:
49+
raise HTTPException(status_code=400, detail=f"Webhook error: {e}")
50+
51+
# event is verified — handle it
52+
result = await service.handle_webhook_event(event)
53+
return result

src/api/v1/products/__init__.py

Whitespace-only changes.

src/api/v1/products/images.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from __future__ import annotations
2+
3+
from typing import List
4+
5+
from fastapi import APIRouter, Depends, UploadFile, status
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from db.dependencies.auth import ADMIN_ROLE, SELLER_ROLE, require_roles
9+
from db.dependencies.sessions import get_db_session
10+
from schemas.product import (
11+
ProductImageOutScheme,
12+
)
13+
from services.product_service import ProductImageService
14+
15+
router = APIRouter(prefix="/products", tags=["Product Images"])
16+
17+
18+
async def get_product_images_service(
19+
session: AsyncSession = Depends(get_db_session),
20+
) -> ProductImageService:
21+
return ProductImageService(session)
22+
23+
24+
# Product Image endpoints
25+
@router.get("/{product_id}/images", response_model=List[ProductImageOutScheme])
26+
async def list_images(
27+
product_id: int,
28+
product_service: ProductImageService = Depends(get_product_images_service),
29+
):
30+
return await product_service.list_images(product_id)
31+
32+
33+
@router.post(
34+
"/{product_id}/images",
35+
response_model=ProductImageOutScheme,
36+
# dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))]
37+
)
38+
async def upload_image(
39+
product_id: int,
40+
file: UploadFile,
41+
product_service: ProductImageService = Depends(get_product_images_service),
42+
):
43+
return await product_service.add_image(product_id, file)
44+
45+
46+
@router.delete(
47+
"/images/{image_id}",
48+
status_code=status.HTTP_204_NO_CONTENT,
49+
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
50+
)
51+
async def delete_image(
52+
image_id: int,
53+
product_service: ProductImageService = Depends(get_product_images_service),
54+
):
55+
await product_service.delete_image(image_id)

0 commit comments

Comments
 (0)