first commit
CI / typecheck (push) Successful in 1m8s
CI / format (push) Failing after 1m6s
CI / lint (push) Failing after 49s
CI / test (push) Failing after 1m8s

This commit is contained in:
SDI
2026-07-15 18:05:12 +09:00
commit 12e4f17b62
4633 changed files with 817125 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""API routing package."""
+9
View File
@@ -0,0 +1,9 @@
from fastapi import APIRouter
from app.api.routes.health import router as health_router
from app.api.routes.tickets import router as workspace_ticket_router, ticket_router
api_router = APIRouter()
api_router.include_router(health_router, prefix="/api")
api_router.include_router(workspace_ticket_router, prefix="/api")
api_router.include_router(ticket_router, prefix="/api")
@@ -0,0 +1 @@
"""API route modules."""
@@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter(tags=["health"])
@router.get("/health")
def health_check() -> dict[str, str]:
return {"status": "ok"}
@@ -0,0 +1,187 @@
import json
from typing import cast
from fastapi import APIRouter, Depends, Request, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from starlette.datastructures import UploadFile as StarletteUploadFile
from app.db.session import get_db
from app.schemas.ticket import (
SupportTicketCommentRecord,
SupportTicketRecord,
TicketCommentCreateRequest,
TicketIssueLinkStatusSyncRequest,
TicketCommentUpdateRequest,
TicketCreateRequest,
TicketCreateResponse,
TicketMutationResponse,
WorkspaceFormTemplateResponse,
WorkspaceSummary,
)
from app.services.ticket_service import TicketService
router = APIRouter(prefix="/workspaces", tags=["tickets"])
service = TicketService()
@router.get("", response_model=list[WorkspaceSummary])
def list_workspaces(db: Session = Depends(get_db)) -> list[WorkspaceSummary]:
return service.list_workspaces(db)
@router.get("/{workspace_code}/form-template", response_model=WorkspaceFormTemplateResponse)
def get_workspace_form_template(
workspace_code: str,
db: Session = Depends(get_db),
) -> WorkspaceFormTemplateResponse:
return service.get_workspace_form_template(db, workspace_code)
@router.post("/{workspace_code}/tickets", response_model=TicketCreateResponse)
async def create_ticket(
workspace_code: str,
request: Request,
db: Session = Depends(get_db),
) -> TicketCreateResponse:
content_type = request.headers.get("content-type", "")
if content_type.startswith("multipart/form-data"):
form = await request.form()
extra_fields_value = form.get("extra_fields")
extra_fields = (
json.loads(extra_fields_value)
if isinstance(extra_fields_value, str) and extra_fields_value
else {}
)
attachments = [
cast(UploadFile, value)
for key, value in form.multi_items()
if key == "attachments"
and isinstance(value, (UploadFile, StarletteUploadFile))
]
request_payload = TicketCreateRequest(
workspace_code=workspace_code,
requester_id=str(form.get("requester_id") or ""),
requester_tenant_id=str(form.get("requester_tenant_id") or ""),
requester_contact=str(form.get("requester_contact") or ""),
title=str(form.get("title") or ""),
description=str(form.get("description") or ""),
category_code=str(form.get("category_code") or ""),
ticket_type=str(form.get("ticket_type") or "GENERAL"),
requires_approval=str(form.get("requires_approval") or "false").lower() == "true",
extra_fields=extra_fields,
)
return service.create_ticket(db, request_payload, attachments)
payload = TicketCreateRequest.model_validate(await request.json())
request_payload = payload.model_copy(update={"workspace_code": workspace_code})
return service.create_ticket(db, request_payload)
@router.get("/{workspace_code}/tickets", response_model=list[SupportTicketRecord])
def list_workspace_tickets(
workspace_code: str,
requesterId: str | None = None,
requesterTenantId: str | None = None,
db: Session = Depends(get_db),
) -> list[SupportTicketRecord]:
return service.list_workspace_tickets(db, workspace_code, requesterId, requesterTenantId)
@router.get("/../tickets", include_in_schema=False)
def _noop_redirect() -> None:
return None
ticket_router = APIRouter(prefix="/tickets", tags=["support-tickets"])
@ticket_router.get("", response_model=list[SupportTicketRecord])
def list_tickets(db: Session = Depends(get_db)) -> list[SupportTicketRecord]:
return service.list_tickets(db)
@ticket_router.get("/{ticket_id}", response_model=SupportTicketRecord)
def get_ticket(
ticket_id: int,
workspaceCode: str | None = None,
db: Session = Depends(get_db),
) -> SupportTicketRecord:
return service.get_ticket(db, ticket_id, workspaceCode)
@ticket_router.get("/{ticket_id}/attachments/{attachment_id}")
def download_ticket_attachment(
ticket_id: int,
attachment_id: int,
workspaceCode: str | None = None,
db: Session = Depends(get_db),
) -> FileResponse:
attachment = service.get_ticket_attachment(db, ticket_id, attachment_id, workspaceCode)
file_path = service.resolve_attachment_path(attachment)
return FileResponse(
path=file_path,
media_type=attachment.mime_type or "application/octet-stream",
filename=attachment.original_file_name,
)
@ticket_router.get("/{ticket_id}/comments", response_model=list[SupportTicketCommentRecord])
def list_ticket_comments(
ticket_id: int,
workspaceCode: str | None = None,
db: Session = Depends(get_db),
) -> list[SupportTicketCommentRecord]:
return service.list_ticket_comments(db, ticket_id, workspaceCode)
@ticket_router.post("/{ticket_id}/comments", response_model=SupportTicketCommentRecord)
def create_ticket_comment(
ticket_id: int,
payload: TicketCommentCreateRequest,
workspaceCode: str | None = None,
db: Session = Depends(get_db),
) -> SupportTicketCommentRecord:
return service.create_ticket_comment(db, ticket_id, payload, workspaceCode)
@ticket_router.put("/{ticket_id}/comments/{comment_id}", response_model=SupportTicketCommentRecord)
def update_ticket_comment(
ticket_id: int,
comment_id: int,
payload: TicketCommentUpdateRequest,
db: Session = Depends(get_db),
) -> SupportTicketCommentRecord:
return service.update_ticket_comment(db, ticket_id, comment_id, payload)
@ticket_router.delete("/{ticket_id}/comments/{comment_id}")
def delete_ticket_comment(
ticket_id: int,
comment_id: int,
requesterId: str,
requesterTenantId: str,
db: Session = Depends(get_db),
) -> dict[str, int | bool]:
return service.delete_ticket_comment(db, ticket_id, comment_id, requesterId, requesterTenantId)
@ticket_router.post("/{ticket_id}/approve", response_model=TicketMutationResponse)
def approve_ticket(ticket_id: int, db: Session = Depends(get_db)) -> TicketMutationResponse:
return service.approve_ticket(db, ticket_id)
@ticket_router.post("/{ticket_id}/issue", response_model=TicketMutationResponse)
def create_issue(ticket_id: int, db: Session = Depends(get_db)) -> TicketMutationResponse:
return service.create_issue(db, ticket_id)
@ticket_router.post("/{ticket_id}/issue-link-status")
def sync_issue_link_status(
ticket_id: int,
payload: TicketIssueLinkStatusSyncRequest,
workspaceCode: str | None = None,
db: Session = Depends(get_db),
) -> dict[str, int | str | bool]:
return service.sync_issue_link_status(db, ticket_id, payload.linked, workspaceCode)