Skip to content

Commit fdcffce

Browse files
committed
feat: Implement product moderation workflow with new product statuses, moderator ID tracking, and dedicated API endpoints
1 parent e10ebc2 commit fdcffce

6 files changed

Lines changed: 289 additions & 24 deletions

File tree

app/core/audit_log/service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ async def log_event(
1717
target_id: UUID,
1818
action: str,
1919
changes: dict[str, Any],
20+
extra_data: dict[str, Any] | None = None,
2021
) -> None:
2122
context = get_contextvars()
2223
request_id = context.get('request_id')
2324
remote_ip = context.get('remote_ip')
24-
25+
if extra_data:
26+
changes.update(extra_data)
2527
log = AuditLog(
2628
actor_id=actor_id,
2729
target_type=target_type,
@@ -63,6 +65,7 @@ async def log_object_change(
6365
action: str,
6466
old_obj: BaseModel | None,
6567
new_obj: BaseModel | None,
68+
extra_data: dict[str, Any] | None = None,
6669
) -> None:
6770
diff = self.get_diff(old_obj, new_obj)
6871
if diff:
@@ -73,6 +76,7 @@ async def log_object_change(
7376
target_id=target_id,
7477
action=action,
7578
changes=diff,
79+
extra_data=extra_data,
7680
)
7781

7882

app/services/inventory/deps.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Annotated
2+
3+
from fastapi import Depends
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from app.core.database import get_session
7+
from app.services.inventory.service import InventoryAdminService, InventoryService
8+
9+
10+
async def get_inventory_service() -> InventoryService:
11+
return InventoryService()
12+
13+
14+
async def get_inventory_admin_service() -> InventoryAdminService:
15+
return InventoryAdminService()
16+
17+
18+
InventoryServiceDep = Annotated[InventoryService, Depends(get_inventory_service)]
19+
InventoryAdminServiceDep = Annotated[
20+
InventoryAdminService, Depends(get_inventory_admin_service)
21+
]
22+
SessionDep = Annotated[AsyncSession, Depends(get_session)]

app/services/inventory/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class ProductStatus(StrEnum):
2020
DRAFT = 'DRAFT'
2121
ACTIVE = 'ACTIVE'
2222
ARCHIVED = 'ARCHIVED'
23+
PENDING_MODERATION = 'PENDING_MODERATION'
24+
MODERATION_IN_PROGRESS = 'MODERATION_IN_PROGRESS'
25+
REJECTED = 'REJECTED'
2326

2427

2528
class Product(Base):
@@ -46,6 +49,9 @@ class Product(Base):
4649
status: Mapped[ProductStatus] = mapped_column(
4750
SQLEnum(ProductStatus), nullable=False, default=ProductStatus.DRAFT
4851
)
52+
moderator_id: Mapped[UUID | None] = mapped_column(
53+
ForeignKey('users.id'), nullable=True, index=True
54+
)
4955
if TYPE_CHECKING:
5056
from app.services.media.models import ProductImage
5157
images: Mapped[list['ProductImage']] = relationship(

app/services/inventory/routes.py

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Annotated
22
from uuid import UUID
33

4-
from fastapi import APIRouter, Depends, Header, Request, status
4+
from fastapi import APIRouter, Depends, Header, Query, Request, status
55
from sqlalchemy.ext.asyncio import AsyncSession
66

77
from app.core.database import get_session
@@ -15,11 +15,16 @@
1515
ReservationCreate,
1616
ReservationResponse,
1717
)
18-
from app.services.inventory.service import InventoryService
18+
from app.services.inventory.service import InventoryAdminService, InventoryService
1919
from app.services.user.models import User
2020
from app.shared.decorators import idempotent
2121
from app.shared.deps import get_current_user
2222

23+
from .deps import (
24+
get_inventory_admin_service,
25+
get_inventory_service,
26+
)
27+
2328
router_v1 = APIRouter(prefix='/inventory', tags=['Inventory'])
2429

2530
SELLER_DEPENDENCY = Depends(
@@ -48,10 +53,11 @@
4853
@router_v1.get('/', response_model=list[ProductRead])
4954
async def get_active_products(
5055
session: Annotated[AsyncSession, Depends(get_session)],
56+
service: Annotated[InventoryService, Depends(get_inventory_service)],
5157
skip: int = 0,
5258
limit: int = 50,
5359
) -> list[ProductRead]:
54-
products = await InventoryService.get_products(
60+
products = await service.get_products(
5561
status=ProductStatus.ACTIVE,
5662
skip=skip,
5763
limit=limit,
@@ -65,8 +71,9 @@ async def create_product(
6571
product_data: ProductCreate,
6672
session: Annotated[AsyncSession, Depends(get_session)],
6773
current_user: Annotated[User, SELLER_DEPENDENCY],
74+
service: Annotated[InventoryService, Depends(get_inventory_service)],
6875
) -> ProductRead:
69-
product = await InventoryService.create_product(
76+
product = await service.create_product(
7077
current_user=current_user,
7178
session=session,
7279
product_data=product_data,
@@ -80,8 +87,9 @@ async def activate_product(
8087
product_id: UUID,
8188
session: Annotated[AsyncSession, Depends(get_session)],
8289
current_user: Annotated[User, ADMIN_DEPENDENCY],
90+
service: Annotated[InventoryAdminService, Depends(get_inventory_admin_service)],
8391
) -> ProductRead:
84-
product = await InventoryService.change_status(
92+
product = await service.change_status(
8593
session=session,
8694
product_id=product_id,
8795
status=ProductStatus.ACTIVE,
@@ -96,8 +104,9 @@ async def update_product(
96104
product_data: ProductUpdate,
97105
session: Annotated[AsyncSession, Depends(get_session)],
98106
current_user: Annotated[User, ADMIN_AND_SELLER_DEPENDENCY],
107+
service: Annotated[InventoryService, Depends(get_inventory_service)],
99108
) -> ProductRead:
100-
product = await InventoryService.update_product(
109+
product = await service.update_product(
101110
session=session,
102111
product_id=product_id,
103112
product_data=product_data,
@@ -111,8 +120,9 @@ async def delete_product(
111120
product_id: UUID,
112121
session: Annotated[AsyncSession, Depends(get_session)],
113122
current_user: Annotated[User, ADMIN_DEPENDENCY],
123+
service: Annotated[InventoryService, Depends(get_inventory_service)],
114124
) -> None:
115-
await InventoryService.delete_product(
125+
await service.delete_product(
116126
session=session,
117127
product_id=product_id,
118128
current_user=current_user,
@@ -123,8 +133,9 @@ async def delete_product(
123133
async def get_product(
124134
product_id: UUID,
125135
session: Annotated[AsyncSession, Depends(get_session)],
136+
service: Annotated[InventoryService, Depends(get_inventory_service)],
126137
) -> ProductRead:
127-
product = await InventoryService.get_product(
138+
product = await service.get_product(
128139
session=session,
129140
product_id=product_id,
130141
)
@@ -136,6 +147,7 @@ async def get_product(
136147
async def reservation_data(
137148
request: Request,
138149
reservation_data: ReservationCreate,
150+
service: Annotated[InventoryService, Depends(get_inventory_service)],
139151
x_idempotency_key: str = Header(...),
140152
session: AsyncSession = Depends(get_session),
141153
current_user: User = Depends(get_current_user),
@@ -145,10 +157,72 @@ async def reservation_data(
145157
user_id=str(current_user.id),
146158
item_id=str(reservation_data.product_id),
147159
)
148-
result = await InventoryService.reserve_items(
160+
result = await service.reserve_items(
149161
session=session,
150162
user_id=current_user.id,
151163
idempotency_key=x_idempotency_key,
152164
reservation_data=reservation_data,
153165
)
154166
return ReservationResponse.model_validate(result)
167+
168+
169+
@router_v1.post('/{product_id}/submit', response_model=ProductRead)
170+
async def submit_for_moderation(
171+
product_id: UUID,
172+
session: Annotated[AsyncSession, Depends(get_session)],
173+
current_user: Annotated[User, SELLER_DEPENDENCY],
174+
service: Annotated[InventoryService, Depends(get_inventory_service)],
175+
) -> ProductRead:
176+
product = await service.submit_for_moderation(
177+
session=session,
178+
product_id=product_id,
179+
current_user=current_user,
180+
)
181+
return ProductRead.model_validate(product)
182+
183+
184+
@router_v1.post('/{product_id}/approve', response_model=ProductRead)
185+
async def approve_product(
186+
product_id: UUID,
187+
session: Annotated[AsyncSession, Depends(get_session)],
188+
current_user: Annotated[User, ADMIN_DEPENDENCY],
189+
service: Annotated[InventoryAdminService, Depends(get_inventory_admin_service)],
190+
) -> ProductRead:
191+
product = await service.approve_product(
192+
session=session,
193+
product_id=product_id,
194+
moderator_user=current_user,
195+
)
196+
return ProductRead.model_validate(product)
197+
198+
199+
@router_v1.post('/{product_id}/reject', response_model=ProductRead)
200+
async def reject_product(
201+
product_id: UUID,
202+
session: Annotated[AsyncSession, Depends(get_session)],
203+
current_user: Annotated[User, ADMIN_DEPENDENCY],
204+
service: Annotated[InventoryAdminService, Depends(get_inventory_admin_service)],
205+
reason: str = Query(...),
206+
) -> ProductRead:
207+
product = await service.reject_product(
208+
session=session,
209+
product_id=product_id,
210+
moderator_user=current_user,
211+
reason=reason,
212+
)
213+
return ProductRead.model_validate(product)
214+
215+
216+
@router_v1.post('/{product_id}/claim', response_model=ProductRead)
217+
async def claim_for_moderation(
218+
product_id: UUID,
219+
session: Annotated[AsyncSession, Depends(get_session)],
220+
current_user: Annotated[User, ADMIN_DEPENDENCY],
221+
service: Annotated[InventoryAdminService, Depends(get_inventory_admin_service)],
222+
) -> ProductRead:
223+
product = await service.claim_for_moderation(
224+
session=session,
225+
product_id=product_id,
226+
moderator_user=current_user,
227+
)
228+
return ProductRead.model_validate(product)

0 commit comments

Comments
 (0)