refactor: migrate templates and static files to self-contained modules
Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
"""
|
||||
Messaging module API routes.
|
||||
|
||||
Admin routes:
|
||||
- /messages/* - Conversation and message management
|
||||
- /notifications/* - Admin notifications and platform alerts
|
||||
|
||||
Vendor routes:
|
||||
- /messages/* - Conversation and message management
|
||||
- /notifications/* - Vendor notifications
|
||||
@@ -12,10 +16,11 @@ Storefront routes:
|
||||
- Customer-facing messaging
|
||||
"""
|
||||
|
||||
from app.modules.messaging.routes.api.admin import admin_router
|
||||
from app.modules.messaging.routes.api.storefront import router as storefront_router
|
||||
from app.modules.messaging.routes.api.vendor import vendor_router
|
||||
|
||||
# Tag for OpenAPI documentation
|
||||
STOREFRONT_TAG = "Messages (Storefront)"
|
||||
|
||||
__all__ = ["storefront_router", "vendor_router", "STOREFRONT_TAG"]
|
||||
__all__ = ["admin_router", "storefront_router", "vendor_router", "STOREFRONT_TAG"]
|
||||
|
||||
26
app/modules/messaging/routes/api/admin.py
Normal file
26
app/modules/messaging/routes/api/admin.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# app/modules/messaging/routes/api/admin.py
|
||||
"""
|
||||
Messaging module admin API routes.
|
||||
|
||||
Aggregates all admin messaging routes:
|
||||
- /messages/* - Conversation and message management
|
||||
- /notifications/* - Admin notifications and platform alerts
|
||||
- /email-templates/* - Email template management
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.deps import require_module_access
|
||||
|
||||
from .admin_messages import admin_messages_router
|
||||
from .admin_notifications import admin_notifications_router
|
||||
from .admin_email_templates import admin_email_templates_router
|
||||
|
||||
admin_router = APIRouter(
|
||||
dependencies=[Depends(require_module_access("messaging"))],
|
||||
)
|
||||
|
||||
# Aggregate all messaging admin routes
|
||||
admin_router.include_router(admin_messages_router, tags=["admin-messages"])
|
||||
admin_router.include_router(admin_notifications_router, tags=["admin-notifications"])
|
||||
admin_router.include_router(admin_email_templates_router, tags=["admin-email-templates"])
|
||||
356
app/modules/messaging/routes/api/admin_email_templates.py
Normal file
356
app/modules/messaging/routes/api/admin_email_templates.py
Normal file
@@ -0,0 +1,356 @@
|
||||
# app/modules/messaging/routes/api/admin_email_templates.py
|
||||
"""
|
||||
Admin email template management endpoints.
|
||||
|
||||
Allows platform administrators to:
|
||||
- View all email templates
|
||||
- Edit template content for all languages
|
||||
- Preview templates with sample data
|
||||
- Send test emails
|
||||
- View email logs
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.messaging.services.email_service import EmailService
|
||||
from app.modules.messaging.services.email_template_service import EmailTemplateService
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_email_templates_router = APIRouter(prefix="/email-templates")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SCHEMAS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TemplateUpdate(BaseModel):
|
||||
"""Schema for updating a platform template."""
|
||||
|
||||
subject: str = Field(..., min_length=1, max_length=500)
|
||||
body_html: str = Field(..., min_length=1)
|
||||
body_text: str | None = None
|
||||
|
||||
|
||||
class PreviewRequest(BaseModel):
|
||||
"""Schema for previewing a template."""
|
||||
|
||||
template_code: str
|
||||
language: str = "en"
|
||||
variables: dict[str, Any] = {}
|
||||
|
||||
|
||||
class TestEmailRequest(BaseModel):
|
||||
"""Schema for sending a test email."""
|
||||
|
||||
template_code: str
|
||||
language: str = "en"
|
||||
to_email: EmailStr
|
||||
variables: dict[str, Any] = {}
|
||||
|
||||
|
||||
class TemplateListItem(BaseModel):
|
||||
"""Schema for a template in the list."""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
category: str
|
||||
languages: list[str] # Matches service output field name
|
||||
is_platform_only: bool = False
|
||||
variables: list[str] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TemplateListResponse(BaseModel):
|
||||
"""Response schema for listing templates."""
|
||||
|
||||
templates: list[TemplateListItem]
|
||||
|
||||
|
||||
class CategoriesResponse(BaseModel):
|
||||
"""Response schema for template categories."""
|
||||
|
||||
categories: list[str]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ENDPOINTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@admin_email_templates_router.get("", response_model=TemplateListResponse)
|
||||
def list_templates(
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all platform email templates.
|
||||
|
||||
Returns templates grouped by code with available languages.
|
||||
"""
|
||||
service = EmailTemplateService(db)
|
||||
return TemplateListResponse(templates=service.list_platform_templates())
|
||||
|
||||
|
||||
@admin_email_templates_router.get("/categories", response_model=CategoriesResponse)
|
||||
def get_categories(
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get list of email template categories."""
|
||||
service = EmailTemplateService(db)
|
||||
return CategoriesResponse(categories=service.get_template_categories())
|
||||
|
||||
|
||||
@admin_email_templates_router.get("/{code}")
|
||||
def get_template(
|
||||
code: str,
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get a specific template with all language versions.
|
||||
|
||||
Returns template metadata and content for all available languages.
|
||||
"""
|
||||
service = EmailTemplateService(db)
|
||||
return service.get_platform_template(code)
|
||||
|
||||
|
||||
@admin_email_templates_router.get("/{code}/{language}")
|
||||
def get_template_language(
|
||||
code: str,
|
||||
language: str,
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get a specific template for a specific language.
|
||||
|
||||
Returns template content with variables information.
|
||||
"""
|
||||
service = EmailTemplateService(db)
|
||||
template = service.get_platform_template_language(code, language)
|
||||
|
||||
return {
|
||||
"code": template.code,
|
||||
"language": template.language,
|
||||
"name": template.name,
|
||||
"description": template.description,
|
||||
"category": template.category,
|
||||
"subject": template.subject,
|
||||
"body_html": template.body_html,
|
||||
"body_text": template.body_text,
|
||||
"variables": template.variables,
|
||||
"required_variables": template.required_variables,
|
||||
"is_platform_only": template.is_platform_only,
|
||||
}
|
||||
|
||||
|
||||
@admin_email_templates_router.put("/{code}/{language}")
|
||||
def update_template(
|
||||
code: str,
|
||||
language: str,
|
||||
template_data: TemplateUpdate,
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update a platform email template.
|
||||
|
||||
Updates the template content for a specific language.
|
||||
"""
|
||||
service = EmailTemplateService(db)
|
||||
service.update_platform_template(
|
||||
code=code,
|
||||
language=language,
|
||||
subject=template_data.subject,
|
||||
body_html=template_data.body_html,
|
||||
body_text=template_data.body_text,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Template updated successfully"}
|
||||
|
||||
|
||||
@admin_email_templates_router.post("/{code}/preview")
|
||||
def preview_template(
|
||||
code: str,
|
||||
preview_data: PreviewRequest,
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Preview a template with sample variables.
|
||||
|
||||
Renders the template with provided variables and returns the result.
|
||||
"""
|
||||
service = EmailTemplateService(db)
|
||||
|
||||
# Merge with sample variables if not provided
|
||||
variables = {
|
||||
**_get_sample_variables(code),
|
||||
**preview_data.variables,
|
||||
}
|
||||
|
||||
return service.preview_template(code, preview_data.language, variables)
|
||||
|
||||
|
||||
@admin_email_templates_router.post("/{code}/test")
|
||||
def send_test_email(
|
||||
code: str,
|
||||
test_data: TestEmailRequest,
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Send a test email using the template.
|
||||
|
||||
Sends the template to the specified email address with sample data.
|
||||
"""
|
||||
# Merge with sample variables
|
||||
variables = {
|
||||
**_get_sample_variables(code),
|
||||
**test_data.variables,
|
||||
}
|
||||
|
||||
try:
|
||||
email_svc = EmailService(db)
|
||||
email_log = email_svc.send_template(
|
||||
template_code=code,
|
||||
to_email=test_data.to_email,
|
||||
variables=variables,
|
||||
language=test_data.language,
|
||||
)
|
||||
|
||||
if email_log.status == "sent":
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Test email sent to {test_data.to_email}",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": email_log.error_message or "Failed to send email",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to send test email: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": str(e),
|
||||
}
|
||||
|
||||
|
||||
@admin_email_templates_router.get("/{code}/logs")
|
||||
def get_template_logs(
|
||||
code: str,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get email logs for a specific template.
|
||||
|
||||
Returns recent email send attempts for the template.
|
||||
"""
|
||||
service = EmailTemplateService(db)
|
||||
logs, total = service.get_template_logs(code, limit, offset)
|
||||
|
||||
return {
|
||||
"logs": logs,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPERS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _get_sample_variables(template_code: str) -> dict[str, Any]:
|
||||
"""Get sample variables for testing templates."""
|
||||
samples = {
|
||||
"signup_welcome": {
|
||||
"first_name": "John",
|
||||
"company_name": "Acme Corp",
|
||||
"email": "john@example.com",
|
||||
"vendor_code": "acme",
|
||||
"login_url": "https://example.com/login",
|
||||
"trial_days": "14",
|
||||
"tier_name": "Business",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
"order_confirmation": {
|
||||
"customer_name": "Jane Doe",
|
||||
"order_number": "ORD-12345",
|
||||
"order_total": "€99.99",
|
||||
"order_items_count": "3",
|
||||
"order_date": "2024-01-15",
|
||||
"shipping_address": "123 Main St, Luxembourg City, L-1234",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
"password_reset": {
|
||||
"customer_name": "John Doe",
|
||||
"reset_link": "https://example.com/reset?token=abc123",
|
||||
"expiry_hours": "1",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
"team_invite": {
|
||||
"invitee_name": "Jane",
|
||||
"inviter_name": "John",
|
||||
"vendor_name": "Acme Corp",
|
||||
"role": "Admin",
|
||||
"accept_url": "https://example.com/accept",
|
||||
"expires_in_days": "7",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
"subscription_welcome": {
|
||||
"vendor_name": "Acme Corp",
|
||||
"tier_name": "Business",
|
||||
"billing_cycle": "Monthly",
|
||||
"amount": "€49.99",
|
||||
"next_billing_date": "2024-02-15",
|
||||
"dashboard_url": "https://example.com/dashboard",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
"payment_failed": {
|
||||
"vendor_name": "Acme Corp",
|
||||
"tier_name": "Business",
|
||||
"amount": "€49.99",
|
||||
"retry_date": "2024-01-18",
|
||||
"update_payment_url": "https://example.com/billing",
|
||||
"support_email": "support@wizamart.com",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
"subscription_cancelled": {
|
||||
"vendor_name": "Acme Corp",
|
||||
"tier_name": "Business",
|
||||
"end_date": "2024-02-15",
|
||||
"reactivate_url": "https://example.com/billing",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
"trial_ending": {
|
||||
"vendor_name": "Acme Corp",
|
||||
"tier_name": "Business",
|
||||
"days_remaining": "3",
|
||||
"trial_end_date": "2024-01-18",
|
||||
"upgrade_url": "https://example.com/upgrade",
|
||||
"features_list": "Unlimited products, API access, Priority support",
|
||||
"platform_name": "Wizamart",
|
||||
},
|
||||
}
|
||||
return samples.get(template_code, {"platform_name": "Wizamart"})
|
||||
624
app/modules/messaging/routes/api/admin_messages.py
Normal file
624
app/modules/messaging/routes/api/admin_messages.py
Normal file
@@ -0,0 +1,624 @@
|
||||
# app/modules/messaging/routes/api/admin_messages.py
|
||||
"""
|
||||
Admin messaging endpoints.
|
||||
|
||||
Provides endpoints for:
|
||||
- Viewing conversations (admin_vendor and admin_customer channels)
|
||||
- Sending and receiving messages
|
||||
- Managing conversation status
|
||||
- File attachments
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.messaging.exceptions import (
|
||||
ConversationClosedException,
|
||||
ConversationNotFoundException,
|
||||
InvalidConversationTypeException,
|
||||
InvalidRecipientTypeException,
|
||||
MessageAttachmentException,
|
||||
)
|
||||
from app.modules.messaging.services.message_attachment_service import message_attachment_service
|
||||
from app.modules.messaging.services.messaging_service import messaging_service
|
||||
from app.modules.messaging.models import ConversationType, ParticipantType
|
||||
from app.modules.messaging.schemas import (
|
||||
AdminConversationListResponse,
|
||||
AdminConversationSummary,
|
||||
AttachmentResponse,
|
||||
CloseConversationResponse,
|
||||
ConversationCreate,
|
||||
ConversationDetailResponse,
|
||||
MarkReadResponse,
|
||||
MessageCreate,
|
||||
MessageResponse,
|
||||
NotificationPreferencesUpdate,
|
||||
ParticipantInfo,
|
||||
ParticipantResponse,
|
||||
RecipientListResponse,
|
||||
RecipientOption,
|
||||
ReopenConversationResponse,
|
||||
UnreadCountResponse,
|
||||
)
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_messages_router = APIRouter(prefix="/messages")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _enrich_message(
|
||||
db: Session, message: Any, include_attachments: bool = True
|
||||
) -> MessageResponse:
|
||||
"""Enrich message with sender info and attachments."""
|
||||
sender_info = messaging_service.get_participant_info(
|
||||
db, message.sender_type, message.sender_id
|
||||
)
|
||||
|
||||
attachments = []
|
||||
if include_attachments and message.attachments:
|
||||
for att in message.attachments:
|
||||
attachments.append(
|
||||
AttachmentResponse(
|
||||
id=att.id,
|
||||
filename=att.filename,
|
||||
original_filename=att.original_filename,
|
||||
file_size=att.file_size,
|
||||
mime_type=att.mime_type,
|
||||
is_image=att.is_image,
|
||||
image_width=att.image_width,
|
||||
image_height=att.image_height,
|
||||
download_url=message_attachment_service.get_download_url(
|
||||
att.file_path
|
||||
),
|
||||
thumbnail_url=(
|
||||
message_attachment_service.get_download_url(att.thumbnail_path)
|
||||
if att.thumbnail_path
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return MessageResponse(
|
||||
id=message.id,
|
||||
conversation_id=message.conversation_id,
|
||||
sender_type=message.sender_type,
|
||||
sender_id=message.sender_id,
|
||||
content=message.content,
|
||||
is_system_message=message.is_system_message,
|
||||
is_deleted=message.is_deleted,
|
||||
created_at=message.created_at,
|
||||
sender_name=sender_info["name"] if sender_info else None,
|
||||
sender_email=sender_info["email"] if sender_info else None,
|
||||
attachments=attachments,
|
||||
)
|
||||
|
||||
|
||||
def _enrich_conversation_summary(
|
||||
db: Session, conversation: Any, current_user_id: int
|
||||
) -> AdminConversationSummary:
|
||||
"""Enrich conversation with other participant info and unread count."""
|
||||
# Get current user's participant record
|
||||
my_participant = next(
|
||||
(
|
||||
p
|
||||
for p in conversation.participants
|
||||
if p.participant_type == ParticipantType.ADMIN
|
||||
and p.participant_id == current_user_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
unread_count = my_participant.unread_count if my_participant else 0
|
||||
|
||||
# Get other participant info
|
||||
other = messaging_service.get_other_participant(
|
||||
conversation, ParticipantType.ADMIN, current_user_id
|
||||
)
|
||||
other_info = None
|
||||
if other:
|
||||
info = messaging_service.get_participant_info(
|
||||
db, other.participant_type, other.participant_id
|
||||
)
|
||||
if info:
|
||||
other_info = ParticipantInfo(
|
||||
id=info["id"],
|
||||
type=info["type"],
|
||||
name=info["name"],
|
||||
email=info.get("email"),
|
||||
)
|
||||
|
||||
# Get last message preview
|
||||
last_message_preview = None
|
||||
if conversation.messages:
|
||||
last_msg = conversation.messages[-1] if conversation.messages else None
|
||||
if last_msg:
|
||||
preview = last_msg.content[:100]
|
||||
if len(last_msg.content) > 100:
|
||||
preview += "..."
|
||||
last_message_preview = preview
|
||||
|
||||
# Get vendor info if applicable
|
||||
vendor_name = None
|
||||
vendor_code = None
|
||||
if conversation.vendor:
|
||||
vendor_name = conversation.vendor.name
|
||||
vendor_code = conversation.vendor.vendor_code
|
||||
|
||||
return AdminConversationSummary(
|
||||
id=conversation.id,
|
||||
conversation_type=conversation.conversation_type,
|
||||
subject=conversation.subject,
|
||||
vendor_id=conversation.vendor_id,
|
||||
is_closed=conversation.is_closed,
|
||||
closed_at=conversation.closed_at,
|
||||
last_message_at=conversation.last_message_at,
|
||||
message_count=conversation.message_count,
|
||||
created_at=conversation.created_at,
|
||||
unread_count=unread_count,
|
||||
other_participant=other_info,
|
||||
last_message_preview=last_message_preview,
|
||||
vendor_name=vendor_name,
|
||||
vendor_code=vendor_code,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONVERSATION LIST
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_messages_router.get("", response_model=AdminConversationListResponse)
|
||||
def list_conversations(
|
||||
conversation_type: ConversationType | None = Query(None, description="Filter by type"),
|
||||
is_closed: bool | None = Query(None, description="Filter by status"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> AdminConversationListResponse:
|
||||
"""List conversations for admin (admin_vendor and admin_customer channels)."""
|
||||
conversations, total, total_unread = messaging_service.list_conversations(
|
||||
db=db,
|
||||
participant_type=ParticipantType.ADMIN,
|
||||
participant_id=current_admin.id,
|
||||
conversation_type=conversation_type,
|
||||
is_closed=is_closed,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return AdminConversationListResponse(
|
||||
conversations=[
|
||||
_enrich_conversation_summary(db, c, current_admin.id) for c in conversations
|
||||
],
|
||||
total=total,
|
||||
total_unread=total_unread,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@admin_messages_router.get("/unread-count", response_model=UnreadCountResponse)
|
||||
def get_unread_count(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> UnreadCountResponse:
|
||||
"""Get total unread message count for header badge."""
|
||||
count = messaging_service.get_unread_count(
|
||||
db=db,
|
||||
participant_type=ParticipantType.ADMIN,
|
||||
participant_id=current_admin.id,
|
||||
)
|
||||
return UnreadCountResponse(total_unread=count)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# RECIPIENTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_messages_router.get("/recipients", response_model=RecipientListResponse)
|
||||
def get_recipients(
|
||||
recipient_type: ParticipantType = Query(..., description="Type of recipients to list"),
|
||||
search: str | None = Query(None, description="Search by name/email"),
|
||||
vendor_id: int | None = Query(None, description="Filter by vendor"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> RecipientListResponse:
|
||||
"""Get list of available recipients for compose modal."""
|
||||
if recipient_type == ParticipantType.VENDOR:
|
||||
recipient_data, total = messaging_service.get_vendor_recipients(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
search=search,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
recipients = [
|
||||
RecipientOption(
|
||||
id=r["id"],
|
||||
type=r["type"],
|
||||
name=r["name"],
|
||||
email=r["email"],
|
||||
vendor_id=r["vendor_id"],
|
||||
vendor_name=r.get("vendor_name"),
|
||||
)
|
||||
for r in recipient_data
|
||||
]
|
||||
elif recipient_type == ParticipantType.CUSTOMER:
|
||||
recipient_data, total = messaging_service.get_customer_recipients(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
search=search,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
recipients = [
|
||||
RecipientOption(
|
||||
id=r["id"],
|
||||
type=r["type"],
|
||||
name=r["name"],
|
||||
email=r["email"],
|
||||
vendor_id=r["vendor_id"],
|
||||
)
|
||||
for r in recipient_data
|
||||
]
|
||||
else:
|
||||
recipients = []
|
||||
total = 0
|
||||
|
||||
return RecipientListResponse(recipients=recipients, total=total)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CREATE CONVERSATION
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_messages_router.post("", response_model=ConversationDetailResponse)
|
||||
def create_conversation(
|
||||
data: ConversationCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> ConversationDetailResponse:
|
||||
"""Create a new conversation."""
|
||||
# Validate conversation type for admin
|
||||
if data.conversation_type not in [
|
||||
ConversationType.ADMIN_VENDOR,
|
||||
ConversationType.ADMIN_CUSTOMER,
|
||||
]:
|
||||
raise InvalidConversationTypeException(
|
||||
message="Admin can only create admin_vendor or admin_customer conversations",
|
||||
allowed_types=["admin_vendor", "admin_customer"],
|
||||
)
|
||||
|
||||
# Validate recipient type matches conversation type
|
||||
if (
|
||||
data.conversation_type == ConversationType.ADMIN_VENDOR
|
||||
and data.recipient_type != ParticipantType.VENDOR
|
||||
):
|
||||
raise InvalidRecipientTypeException(
|
||||
conversation_type="admin_vendor",
|
||||
expected_recipient_type="vendor",
|
||||
)
|
||||
if (
|
||||
data.conversation_type == ConversationType.ADMIN_CUSTOMER
|
||||
and data.recipient_type != ParticipantType.CUSTOMER
|
||||
):
|
||||
raise InvalidRecipientTypeException(
|
||||
conversation_type="admin_customer",
|
||||
expected_recipient_type="customer",
|
||||
)
|
||||
|
||||
# Create conversation
|
||||
conversation = messaging_service.create_conversation(
|
||||
db=db,
|
||||
conversation_type=data.conversation_type,
|
||||
subject=data.subject,
|
||||
initiator_type=ParticipantType.ADMIN,
|
||||
initiator_id=current_admin.id,
|
||||
recipient_type=data.recipient_type,
|
||||
recipient_id=data.recipient_id,
|
||||
vendor_id=data.vendor_id,
|
||||
initial_message=data.initial_message,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(conversation)
|
||||
|
||||
logger.info(
|
||||
f"Admin {current_admin.username} created conversation {conversation.id} "
|
||||
f"with {data.recipient_type.value}:{data.recipient_id}"
|
||||
)
|
||||
|
||||
# Return full detail response
|
||||
return _build_conversation_detail(db, conversation, current_admin.id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONVERSATION DETAIL
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _build_conversation_detail(
|
||||
db: Session, conversation: Any, current_user_id: int
|
||||
) -> ConversationDetailResponse:
|
||||
"""Build full conversation detail response."""
|
||||
# Get my participant for unread count
|
||||
my_participant = next(
|
||||
(
|
||||
p
|
||||
for p in conversation.participants
|
||||
if p.participant_type == ParticipantType.ADMIN
|
||||
and p.participant_id == current_user_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
unread_count = my_participant.unread_count if my_participant else 0
|
||||
|
||||
# Build participant responses
|
||||
participants = []
|
||||
for p in conversation.participants:
|
||||
info = messaging_service.get_participant_info(
|
||||
db, p.participant_type, p.participant_id
|
||||
)
|
||||
participants.append(
|
||||
ParticipantResponse(
|
||||
id=p.id,
|
||||
participant_type=p.participant_type,
|
||||
participant_id=p.participant_id,
|
||||
unread_count=p.unread_count,
|
||||
last_read_at=p.last_read_at,
|
||||
email_notifications=p.email_notifications,
|
||||
muted=p.muted,
|
||||
participant_info=(
|
||||
ParticipantInfo(
|
||||
id=info["id"],
|
||||
type=info["type"],
|
||||
name=info["name"],
|
||||
email=info.get("email"),
|
||||
)
|
||||
if info
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Build message responses
|
||||
messages = [_enrich_message(db, m) for m in conversation.messages]
|
||||
|
||||
# Get vendor name if applicable
|
||||
vendor_name = None
|
||||
if conversation.vendor:
|
||||
vendor_name = conversation.vendor.name
|
||||
|
||||
return ConversationDetailResponse(
|
||||
id=conversation.id,
|
||||
conversation_type=conversation.conversation_type,
|
||||
subject=conversation.subject,
|
||||
vendor_id=conversation.vendor_id,
|
||||
is_closed=conversation.is_closed,
|
||||
closed_at=conversation.closed_at,
|
||||
closed_by_type=conversation.closed_by_type,
|
||||
closed_by_id=conversation.closed_by_id,
|
||||
last_message_at=conversation.last_message_at,
|
||||
message_count=conversation.message_count,
|
||||
created_at=conversation.created_at,
|
||||
updated_at=conversation.updated_at,
|
||||
participants=participants,
|
||||
messages=messages,
|
||||
unread_count=unread_count,
|
||||
vendor_name=vendor_name,
|
||||
)
|
||||
|
||||
|
||||
@admin_messages_router.get("/{conversation_id}", response_model=ConversationDetailResponse)
|
||||
def get_conversation(
|
||||
conversation_id: int,
|
||||
mark_read: bool = Query(True, description="Automatically mark as read"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> ConversationDetailResponse:
|
||||
"""Get conversation detail with messages."""
|
||||
conversation = messaging_service.get_conversation(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
participant_type=ParticipantType.ADMIN,
|
||||
participant_id=current_admin.id,
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
raise ConversationNotFoundException(str(conversation_id))
|
||||
|
||||
# Mark as read if requested
|
||||
if mark_read:
|
||||
messaging_service.mark_conversation_read(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
reader_type=ParticipantType.ADMIN,
|
||||
reader_id=current_admin.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return _build_conversation_detail(db, conversation, current_admin.id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SEND MESSAGE
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_messages_router.post("/{conversation_id}/messages", response_model=MessageResponse)
|
||||
async def send_message(
|
||||
conversation_id: int,
|
||||
content: str = Form(...),
|
||||
files: list[UploadFile] = File(default=[]),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> MessageResponse:
|
||||
"""Send a message in a conversation, optionally with attachments."""
|
||||
# Verify access
|
||||
conversation = messaging_service.get_conversation(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
participant_type=ParticipantType.ADMIN,
|
||||
participant_id=current_admin.id,
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
raise ConversationNotFoundException(str(conversation_id))
|
||||
|
||||
if conversation.is_closed:
|
||||
raise ConversationClosedException(conversation_id)
|
||||
|
||||
# Process attachments
|
||||
attachments = []
|
||||
for file in files:
|
||||
try:
|
||||
att_data = await message_attachment_service.validate_and_store(
|
||||
db=db, file=file, conversation_id=conversation_id
|
||||
)
|
||||
attachments.append(att_data)
|
||||
except ValueError as e:
|
||||
raise MessageAttachmentException(str(e))
|
||||
|
||||
# Send message
|
||||
message = messaging_service.send_message(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
sender_type=ParticipantType.ADMIN,
|
||||
sender_id=current_admin.id,
|
||||
content=content,
|
||||
attachments=attachments if attachments else None,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
|
||||
logger.info(
|
||||
f"Admin {current_admin.username} sent message {message.id} "
|
||||
f"in conversation {conversation_id}"
|
||||
)
|
||||
|
||||
return _enrich_message(db, message)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONVERSATION ACTIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_messages_router.post("/{conversation_id}/close", response_model=CloseConversationResponse)
|
||||
def close_conversation(
|
||||
conversation_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> CloseConversationResponse:
|
||||
"""Close a conversation."""
|
||||
conversation = messaging_service.close_conversation(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
closer_type=ParticipantType.ADMIN,
|
||||
closer_id=current_admin.id,
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
raise ConversationNotFoundException(str(conversation_id))
|
||||
|
||||
db.commit()
|
||||
logger.info(
|
||||
f"Admin {current_admin.username} closed conversation {conversation_id}"
|
||||
)
|
||||
|
||||
return CloseConversationResponse(
|
||||
success=True,
|
||||
message="Conversation closed",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
|
||||
@admin_messages_router.post("/{conversation_id}/reopen", response_model=ReopenConversationResponse)
|
||||
def reopen_conversation(
|
||||
conversation_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> ReopenConversationResponse:
|
||||
"""Reopen a closed conversation."""
|
||||
conversation = messaging_service.reopen_conversation(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
opener_type=ParticipantType.ADMIN,
|
||||
opener_id=current_admin.id,
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
raise ConversationNotFoundException(str(conversation_id))
|
||||
|
||||
db.commit()
|
||||
logger.info(
|
||||
f"Admin {current_admin.username} reopened conversation {conversation_id}"
|
||||
)
|
||||
|
||||
return ReopenConversationResponse(
|
||||
success=True,
|
||||
message="Conversation reopened",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
|
||||
@admin_messages_router.put("/{conversation_id}/read", response_model=MarkReadResponse)
|
||||
def mark_read(
|
||||
conversation_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> MarkReadResponse:
|
||||
"""Mark conversation as read."""
|
||||
success = messaging_service.mark_conversation_read(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
reader_type=ParticipantType.ADMIN,
|
||||
reader_id=current_admin.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return MarkReadResponse(
|
||||
success=success,
|
||||
conversation_id=conversation_id,
|
||||
unread_count=0,
|
||||
)
|
||||
|
||||
|
||||
class PreferencesUpdateResponse(BaseModel):
|
||||
"""Response for preferences update."""
|
||||
success: bool
|
||||
|
||||
|
||||
@admin_messages_router.put("/{conversation_id}/preferences", response_model=PreferencesUpdateResponse)
|
||||
def update_preferences(
|
||||
conversation_id: int,
|
||||
preferences: NotificationPreferencesUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> PreferencesUpdateResponse:
|
||||
"""Update notification preferences for a conversation."""
|
||||
success = messaging_service.update_notification_preferences(
|
||||
db=db,
|
||||
conversation_id=conversation_id,
|
||||
participant_type=ParticipantType.ADMIN,
|
||||
participant_id=current_admin.id,
|
||||
email_notifications=preferences.email_notifications,
|
||||
muted=preferences.muted,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return PreferencesUpdateResponse(success=success)
|
||||
327
app/modules/messaging/routes/api/admin_notifications.py
Normal file
327
app/modules/messaging/routes/api/admin_notifications.py
Normal file
@@ -0,0 +1,327 @@
|
||||
# app/modules/messaging/routes/api/admin_notifications.py
|
||||
"""
|
||||
Admin notifications and platform alerts endpoints.
|
||||
|
||||
Provides endpoints for:
|
||||
- Viewing admin notifications
|
||||
- Managing platform alerts
|
||||
- System health monitoring
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.messaging.services.admin_notification_service import (
|
||||
admin_notification_service,
|
||||
platform_alert_service,
|
||||
)
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.admin import (
|
||||
AdminNotificationCreate,
|
||||
AdminNotificationListResponse,
|
||||
AdminNotificationResponse,
|
||||
PlatformAlertCreate,
|
||||
PlatformAlertListResponse,
|
||||
PlatformAlertResolve,
|
||||
PlatformAlertResponse,
|
||||
)
|
||||
from app.modules.messaging.schemas import (
|
||||
AlertStatisticsResponse,
|
||||
MessageResponse,
|
||||
UnreadCountResponse,
|
||||
)
|
||||
|
||||
admin_notifications_router = APIRouter(prefix="/notifications")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN NOTIFICATIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_notifications_router.get("", response_model=AdminNotificationListResponse)
|
||||
def get_notifications(
|
||||
priority: str | None = Query(None, description="Filter by priority"),
|
||||
notification_type: str | None = Query(None, description="Filter by type"),
|
||||
is_read: bool | None = Query(None, description="Filter by read status"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> AdminNotificationListResponse:
|
||||
"""Get admin notifications with filtering."""
|
||||
notifications, total, unread_count = admin_notification_service.get_notifications(
|
||||
db=db,
|
||||
priority=priority,
|
||||
is_read=is_read,
|
||||
notification_type=notification_type,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return AdminNotificationListResponse(
|
||||
notifications=[
|
||||
AdminNotificationResponse(
|
||||
id=n.id,
|
||||
type=n.type,
|
||||
priority=n.priority,
|
||||
title=n.title,
|
||||
message=n.message,
|
||||
is_read=n.is_read,
|
||||
read_at=n.read_at,
|
||||
read_by_user_id=n.read_by_user_id,
|
||||
action_required=n.action_required,
|
||||
action_url=n.action_url,
|
||||
metadata=n.notification_metadata,
|
||||
created_at=n.created_at,
|
||||
)
|
||||
for n in notifications
|
||||
],
|
||||
total=total,
|
||||
unread_count=unread_count,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@admin_notifications_router.post("", response_model=AdminNotificationResponse)
|
||||
def create_notification(
|
||||
notification_data: AdminNotificationCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> AdminNotificationResponse:
|
||||
"""Create a new admin notification (manual)."""
|
||||
notification = admin_notification_service.create_from_schema(
|
||||
db=db, data=notification_data
|
||||
)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_admin.username} created notification: {notification.title}")
|
||||
|
||||
return AdminNotificationResponse(
|
||||
id=notification.id,
|
||||
type=notification.type,
|
||||
priority=notification.priority,
|
||||
title=notification.title,
|
||||
message=notification.message,
|
||||
is_read=notification.is_read,
|
||||
read_at=notification.read_at,
|
||||
read_by_user_id=notification.read_by_user_id,
|
||||
action_required=notification.action_required,
|
||||
action_url=notification.action_url,
|
||||
metadata=notification.notification_metadata,
|
||||
created_at=notification.created_at,
|
||||
)
|
||||
|
||||
|
||||
@admin_notifications_router.get("/recent")
|
||||
def get_recent_notifications(
|
||||
limit: int = Query(5, ge=1, le=10),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> dict:
|
||||
"""Get recent unread notifications for header dropdown."""
|
||||
notifications = admin_notification_service.get_recent_notifications(
|
||||
db=db, limit=limit
|
||||
)
|
||||
unread_count = admin_notification_service.get_unread_count(db)
|
||||
|
||||
return {
|
||||
"notifications": [
|
||||
{
|
||||
"id": n.id,
|
||||
"type": n.type,
|
||||
"priority": n.priority,
|
||||
"title": n.title,
|
||||
"message": n.message[:100] + "..." if len(n.message) > 100 else n.message,
|
||||
"action_url": n.action_url,
|
||||
"created_at": n.created_at.isoformat(),
|
||||
}
|
||||
for n in notifications
|
||||
],
|
||||
"unread_count": unread_count,
|
||||
}
|
||||
|
||||
|
||||
@admin_notifications_router.get("/unread-count", response_model=UnreadCountResponse)
|
||||
def get_unread_count(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> UnreadCountResponse:
|
||||
"""Get count of unread notifications."""
|
||||
count = admin_notification_service.get_unread_count(db)
|
||||
return UnreadCountResponse(unread_count=count)
|
||||
|
||||
|
||||
@admin_notifications_router.put("/{notification_id}/read", response_model=MessageResponse)
|
||||
def mark_as_read(
|
||||
notification_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> MessageResponse:
|
||||
"""Mark notification as read."""
|
||||
notification = admin_notification_service.mark_as_read(
|
||||
db=db, notification_id=notification_id, user_id=current_admin.id
|
||||
)
|
||||
db.commit()
|
||||
|
||||
if notification:
|
||||
return MessageResponse(message="Notification marked as read")
|
||||
return MessageResponse(message="Notification not found")
|
||||
|
||||
|
||||
@admin_notifications_router.put("/mark-all-read", response_model=MessageResponse)
|
||||
def mark_all_as_read(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> MessageResponse:
|
||||
"""Mark all notifications as read."""
|
||||
count = admin_notification_service.mark_all_as_read(
|
||||
db=db, user_id=current_admin.id
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return MessageResponse(message=f"Marked {count} notifications as read")
|
||||
|
||||
|
||||
@admin_notifications_router.delete("/{notification_id}", response_model=MessageResponse)
|
||||
def delete_notification(
|
||||
notification_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> MessageResponse:
|
||||
"""Delete a notification."""
|
||||
deleted = admin_notification_service.delete_notification(
|
||||
db=db, notification_id=notification_id
|
||||
)
|
||||
db.commit()
|
||||
|
||||
if deleted:
|
||||
return MessageResponse(message="Notification deleted")
|
||||
|
||||
return MessageResponse(message="Notification not found")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PLATFORM ALERTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_notifications_router.get("/alerts", response_model=PlatformAlertListResponse)
|
||||
def get_platform_alerts(
|
||||
severity: str | None = Query(None, description="Filter by severity"),
|
||||
alert_type: str | None = Query(None, description="Filter by alert type"),
|
||||
is_resolved: bool | None = Query(None, description="Filter by resolution status"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> PlatformAlertListResponse:
|
||||
"""Get platform alerts with filtering."""
|
||||
alerts, total, active_count, critical_count = platform_alert_service.get_alerts(
|
||||
db=db,
|
||||
severity=severity,
|
||||
alert_type=alert_type,
|
||||
is_resolved=is_resolved,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return PlatformAlertListResponse(
|
||||
alerts=[
|
||||
PlatformAlertResponse(
|
||||
id=a.id,
|
||||
alert_type=a.alert_type,
|
||||
severity=a.severity,
|
||||
title=a.title,
|
||||
description=a.description,
|
||||
affected_vendors=a.affected_vendors,
|
||||
affected_systems=a.affected_systems,
|
||||
is_resolved=a.is_resolved,
|
||||
resolved_at=a.resolved_at,
|
||||
resolved_by_user_id=a.resolved_by_user_id,
|
||||
resolution_notes=a.resolution_notes,
|
||||
auto_generated=a.auto_generated,
|
||||
occurrence_count=a.occurrence_count,
|
||||
first_occurred_at=a.first_occurred_at,
|
||||
last_occurred_at=a.last_occurred_at,
|
||||
created_at=a.created_at,
|
||||
)
|
||||
for a in alerts
|
||||
],
|
||||
total=total,
|
||||
active_count=active_count,
|
||||
critical_count=critical_count,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@admin_notifications_router.post("/alerts", response_model=PlatformAlertResponse)
|
||||
def create_platform_alert(
|
||||
alert_data: PlatformAlertCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> PlatformAlertResponse:
|
||||
"""Create new platform alert (manual)."""
|
||||
alert = platform_alert_service.create_from_schema(db=db, data=alert_data)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_admin.username} created alert: {alert.title}")
|
||||
|
||||
return PlatformAlertResponse(
|
||||
id=alert.id,
|
||||
alert_type=alert.alert_type,
|
||||
severity=alert.severity,
|
||||
title=alert.title,
|
||||
description=alert.description,
|
||||
affected_vendors=alert.affected_vendors,
|
||||
affected_systems=alert.affected_systems,
|
||||
is_resolved=alert.is_resolved,
|
||||
resolved_at=alert.resolved_at,
|
||||
resolved_by_user_id=alert.resolved_by_user_id,
|
||||
resolution_notes=alert.resolution_notes,
|
||||
auto_generated=alert.auto_generated,
|
||||
occurrence_count=alert.occurrence_count,
|
||||
first_occurred_at=alert.first_occurred_at,
|
||||
last_occurred_at=alert.last_occurred_at,
|
||||
created_at=alert.created_at,
|
||||
)
|
||||
|
||||
|
||||
@admin_notifications_router.put("/alerts/{alert_id}/resolve", response_model=MessageResponse)
|
||||
def resolve_platform_alert(
|
||||
alert_id: int,
|
||||
resolve_data: PlatformAlertResolve,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> MessageResponse:
|
||||
"""Resolve platform alert."""
|
||||
alert = platform_alert_service.resolve_alert(
|
||||
db=db,
|
||||
alert_id=alert_id,
|
||||
user_id=current_admin.id,
|
||||
resolution_notes=resolve_data.resolution_notes,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
if alert:
|
||||
logger.info(f"Admin {current_admin.username} resolved alert {alert_id}")
|
||||
return MessageResponse(message="Alert resolved successfully")
|
||||
|
||||
return MessageResponse(message="Alert not found or already resolved")
|
||||
|
||||
|
||||
@admin_notifications_router.get("/alerts/stats", response_model=AlertStatisticsResponse)
|
||||
def get_alert_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> AlertStatisticsResponse:
|
||||
"""Get alert statistics for dashboard."""
|
||||
stats = platform_alert_service.get_statistics(db)
|
||||
return AlertStatisticsResponse(**stats)
|
||||
@@ -27,12 +27,12 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_customer_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import (
|
||||
from app.modules.messaging.exceptions import (
|
||||
AttachmentNotFoundException,
|
||||
ConversationClosedException,
|
||||
ConversationNotFoundException,
|
||||
VendorNotFoundException,
|
||||
)
|
||||
from app.modules.tenancy.exceptions import VendorNotFoundException
|
||||
from app.modules.customers.schemas import CustomerContext
|
||||
from app.modules.messaging.models.message import ConversationType, ParticipantType
|
||||
from app.modules.messaging.schemas import (
|
||||
|
||||
@@ -20,8 +20,8 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.services.vendor_email_settings_service import VendorEmailSettingsService
|
||||
from app.services.subscription_service import subscription_service
|
||||
from app.modules.cms.services.vendor_email_settings_service import VendorEmailSettingsService
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
vendor_email_settings_router = APIRouter(prefix="/email-settings")
|
||||
|
||||
@@ -17,9 +17,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.services.email_service import EmailService
|
||||
from app.services.email_template_service import EmailTemplateService
|
||||
from app.services.vendor_service import vendor_service
|
||||
from app.modules.messaging.services.email_service import EmailService
|
||||
from app.modules.messaging.services.email_template_service import EmailTemplateService
|
||||
from app.modules.tenancy.services.vendor_service import vendor_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
vendor_email_templates_router = APIRouter(prefix="/email-templates")
|
||||
|
||||
@@ -20,15 +20,15 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import (
|
||||
from app.modules.messaging.exceptions import (
|
||||
ConversationClosedException,
|
||||
ConversationNotFoundException,
|
||||
InvalidConversationTypeException,
|
||||
InvalidRecipientTypeException,
|
||||
MessageAttachmentException,
|
||||
)
|
||||
from app.services.message_attachment_service import message_attachment_service
|
||||
from app.services.messaging_service import messaging_service
|
||||
from app.modules.messaging.services.message_attachment_service import message_attachment_service
|
||||
from app.modules.messaging.services.messaging_service import messaging_service
|
||||
from app.modules.messaging.models import ConversationType, ParticipantType
|
||||
from app.modules.messaging.schemas import (
|
||||
AttachmentResponse,
|
||||
|
||||
@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.services.vendor_service import vendor_service
|
||||
from app.modules.tenancy.services.vendor_service import vendor_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.messaging.schemas import (
|
||||
MessageResponse,
|
||||
|
||||
Reference in New Issue
Block a user