feat: add workspace

This commit is contained in:
Artem Tsyrulnikov
2025-12-13 22:09:55 +03:00
parent 2fdd56c3e0
commit af32e2a507
55 changed files with 677 additions and 227 deletions

View File

@@ -7,6 +7,7 @@ from src.controller.http.external_channels import external_channels_router
from src.controller.http.placements import placements_router
from src.controller.http.target_channels import target_channels_router
from src.controller.http.views import views_router
from src.controller.http.workspaces import workspaces_router
api_router = APIRouter()
@@ -19,5 +20,6 @@ api_v1_router.include_router(creatives_router)
api_v1_router.include_router(placements_router)
api_v1_router.include_router(views_router)
api_v1_router.include_router(analytics_router)
api_v1_router.include_router(workspaces_router)
api_router.include_router(api_v1_router)

View File

@@ -8,16 +8,18 @@ from fastapi.routing import APIRouter
from src import deps, dto
from src.adapter.jwt import JWTPayload
analytics_router = APIRouter(prefix='/analytics', tags=['analytics'])
analytics_router = APIRouter(prefix='/workspaces/{workspace_id}/analytics', tags=['analytics'])
@analytics_router.get('/placements')
async def get_placements_analytics(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
target_channel_id: uuid.UUID | None = None,
) -> dto.GetPlacementsAnalyticsOutput:
input = dto.GetPlacementsAnalyticsInput(
user_id=current_user.user_id,
workspace_id=workspace_id,
target_channel_id=target_channel_id,
)
return await deps.get_usecase().get_placements_analytics(input)
@@ -25,11 +27,13 @@ async def get_placements_analytics(
@analytics_router.get('/creatives')
async def get_creatives_analytics(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
target_channel_id: uuid.UUID | None = None,
) -> dto.GetCreativesAnalyticsOutput:
input = dto.GetCreativesAnalyticsInput(
user_id=current_user.user_id,
workspace_id=workspace_id,
target_channel_id=target_channel_id,
)
return await deps.get_usecase().get_creatives_analytics(input)
@@ -37,11 +41,13 @@ async def get_creatives_analytics(
@analytics_router.get('/external-channels')
async def get_external_channels_analytics(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
target_channel_id: uuid.UUID | None = None,
) -> dto.GetExternalChannelsAnalyticsOutput:
input = dto.GetExternalChannelsAnalyticsInput(
user_id=current_user.user_id,
workspace_id=workspace_id,
target_channel_id=target_channel_id,
)
return await deps.get_usecase().get_external_channels_analytics(input)
@@ -49,6 +55,7 @@ async def get_external_channels_analytics(
@analytics_router.get('/spending')
async def get_spending_analytics(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
target_channel_id: uuid.UUID | None = None,
date_from: datetime.datetime | None = None,
@@ -57,6 +64,7 @@ async def get_spending_analytics(
) -> dto.GetSpendingAnalyticsOutput:
input = dto.GetSpendingAnalyticsInput(
user_id=current_user.user_id,
workspace_id=workspace_id,
target_channel_id=target_channel_id,
date_from=date_from,
date_to=date_to,

View File

@@ -7,17 +7,19 @@ from fastapi.routing import APIRouter
from src import deps, dto
from src.adapter.jwt import JWTPayload
creatives_router = APIRouter(prefix='/creatives', tags=['creatives'])
creatives_router = APIRouter(prefix='/workspaces/{workspace_id}/creatives', tags=['creatives'])
@creatives_router.get('')
async def list_creatives(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
target_channel_id: uuid.UUID | None = None,
include_archived: bool = False,
) -> dto.GetCreativesOutput:
input = dto.GetCreativesInput(
user_id=current_user.user_id,
workspace_id=workspace_id,
target_channel_id=target_channel_id,
include_archived=include_archived,
)
@@ -28,11 +30,13 @@ async def list_creatives(
@creatives_router.get('/{creative_id}')
async def get_creative(
creative_id: uuid.UUID,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.CreativeOutput:
input = dto.GetCreativeInput(
creative_id=creative_id,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
return await deps.get_usecase().get_creative(input=input)
@@ -41,32 +45,37 @@ async def get_creative(
@creatives_router.post('')
async def create_creative(
request: dto.CreateCreativeInput,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.CreativeOutput:
return await deps.get_usecase().create_creative(request, current_user.user_id)
return await deps.get_usecase().create_creative(request, current_user.user_id, workspace_id)
@creatives_router.patch('/{creative_id}')
async def update_creative(
creative_id: uuid.UUID,
request: dto.UpdateCreativeInput,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.CreativeOutput:
return await deps.get_usecase().update_creative(
creative_id=creative_id,
input=request,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
@creatives_router.delete('/{creative_id}')
async def delete_creative(
creative_id: uuid.UUID,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> None:
input = dto.DeleteCreativeInput(
creative_id=creative_id,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
await deps.get_usecase().delete_creative(input)

View File

@@ -7,18 +7,20 @@ from fastapi.routing import APIRouter
from src import deps, dto
from src.adapter.jwt import JWTPayload
external_channels_router = APIRouter(prefix='/external_channels', tags=['external_channels'])
external_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/external_channels', tags=['external_channels'])
@external_channels_router.get('/target/{target_channel_id}')
async def get_external_channels(
target_channel_id: uuid.UUID,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetExternalChannelsOutput:
"""Get all external channels for a specific target channel."""
input = dto.GetExternalChannelsInput(
target_channel_id=target_channel_id,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
return await deps.get_usecase().get_external_channels(input=input)
@@ -27,12 +29,14 @@ async def get_external_channels(
@external_channels_router.post('')
async def create_external_channel(
request: dto.CreateExternalChannelInput,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.ExternalChannelOutput:
"""Create a new external channel and link it to target channels."""
return await deps.get_usecase().create_external_channel(
input=request,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
@@ -40,6 +44,7 @@ async def create_external_channel(
async def update_external_channel(
channel_id: uuid.UUID,
request: dto.UpdateExternalChannelInput,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.ExternalChannelOutput:
"""Update external channel fields (title, username, description, subscribers_count)."""
@@ -47,6 +52,7 @@ async def update_external_channel(
channel_id=channel_id,
input=request,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
@@ -54,6 +60,7 @@ async def update_external_channel(
async def update_external_channel_links(
channel_id: uuid.UUID,
request: dto.UpdateExternalChannelLinksInput,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.ExternalChannelOutput:
"""
@@ -65,18 +72,21 @@ async def update_external_channel_links(
channel_id=channel_id,
input=request,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
@external_channels_router.delete('/{channel_id}')
async def delete_external_channel(
channel_id: uuid.UUID,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> None:
"""Delete an external channel."""
input = dto.DeleteExternalChannelInput(
channel_id=channel_id,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
await deps.get_usecase().delete_external_channel(input=input)

View File

@@ -7,11 +7,12 @@ from fastapi.routing import APIRouter
from src import deps, dto
from src.adapter.jwt import JWTPayload
placements_router = APIRouter(prefix='/placements', tags=['placements'])
placements_router = APIRouter(prefix='/workspaces/{workspace_id}/placements', tags=['placements'])
@placements_router.get('')
async def list_placements(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
target_channel_id: uuid.UUID | None = None,
external_channel_id: uuid.UUID | None = None,
@@ -20,6 +21,7 @@ async def list_placements(
) -> dto.GetPlacementsOutput:
input = dto.GetPlacementsInput(
user_id=current_user.user_id,
workspace_id=workspace_id,
target_channel_id=target_channel_id,
external_channel_id=external_channel_id,
creative_id=creative_id,
@@ -32,11 +34,13 @@ async def list_placements(
@placements_router.get('/{placement_id}')
async def get_placement(
placement_id: uuid.UUID,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.PlacementOutput:
input = dto.GetPlacementInput(
placement_id=placement_id,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
return await deps.get_usecase().get_placement(input=input)
@@ -45,32 +49,37 @@ async def get_placement(
@placements_router.post('')
async def create_placement(
request: dto.CreatePlacementInput,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.PlacementOutput:
return await deps.get_usecase().create_placement(request, current_user.user_id)
return await deps.get_usecase().create_placement(request, current_user.user_id, workspace_id)
@placements_router.patch('/{placement_id}')
async def update_placement(
placement_id: uuid.UUID,
request: dto.UpdatePlacementInput,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.PlacementOutput:
return await deps.get_usecase().update_placement(
placement_id=placement_id,
input=request,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
@placements_router.delete('/{placement_id}')
async def delete_placement(
placement_id: uuid.UUID,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> None:
input = dto.DeletePlacementInput(
placement_id=placement_id,
user_id=current_user.user_id,
workspace_id=workspace_id,
)
await deps.get_usecase().delete_placement(input)

View File

@@ -1,3 +1,4 @@
import uuid
from typing import Annotated
from fastapi import Depends
@@ -6,15 +7,17 @@ from fastapi.routing import APIRouter
from src import deps, dto
from src.adapter.jwt import JWTPayload
target_channels_router = APIRouter(prefix='/target_channels', tags=['target_channels'])
target_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/target_channels', tags=['target_channels'])
@target_channels_router.get('')
async def get_user_target_chans(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetUserTargetChansOutput:
input = dto.GetUserTargetChansInput(
user_id=current_user.user_id,
workspace_id=workspace_id,
)
return await deps.get_usecase().get_user_target_chans(input=input)

View File

@@ -8,12 +8,13 @@ from fastapi.routing import APIRouter
from src import deps, dto
from src.adapter.jwt import JWTPayload
views_router = APIRouter(prefix='/placements/{placement_id}/views', tags=['views'])
views_router = APIRouter(prefix='/workspaces/{workspace_id}/placements/{placement_id}/views', tags=['views'])
@views_router.get('/history')
async def get_views_history(
placement_id: uuid.UUID,
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
from_date: datetime.datetime | None = None,
to_date: datetime.datetime | None = None,
@@ -21,23 +22,9 @@ async def get_views_history(
input_data = dto.GetViewsHistoryInput(
placement_id=placement_id,
user_id=current_user.user_id,
workspace_id=workspace_id,
from_date=from_date,
to_date=to_date,
)
return await deps.get_usecase().get_views_history(input=input_data)
@views_router.post('/manual')
async def update_views_manually(
placement_id: uuid.UUID,
views_count: int,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.PlacementOutput:
input_data = dto.UpdateViewsManuallyInput(
placement_id=placement_id,
user_id=current_user.user_id,
views_count=views_count,
)
return await deps.get_usecase().update_views_manually(input=input_data)

View File

@@ -0,0 +1,42 @@
import uuid
from typing import Annotated
from fastapi import Depends
from fastapi.routing import APIRouter
from src import deps, dto
from src.adapter.jwt import JWTPayload
workspaces_router = APIRouter(prefix='/workspaces', tags=['workspaces'])
@workspaces_router.get('')
async def list_workspaces(
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetWorkspacesOutput:
return await deps.get_usecase().get_workspaces(current_user.user_id)
@workspaces_router.post('')
async def create_workspace(
request: dto.CreateWorkspaceInput,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.CreateWorkspaceOutput:
return await deps.get_usecase().create_workspace(current_user.user_id, request)
@workspaces_router.patch('/{workspace_id}')
async def update_workspace(
workspace_id: uuid.UUID,
request: dto.UpdateWorkspaceInput,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.WorkspaceMembershipOutput:
return await deps.get_usecase().update_workspace(workspace_id, current_user.user_id, request)
@workspaces_router.delete('/{workspace_id}')
async def delete_workspace(
workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> None:
await deps.get_usecase().delete_workspace(workspace_id, current_user.user_id)