sync: Auto-sync from Mikes-MacBook-Air.local at 2026-03-09 08:14:13
Synced files: - Session logs updated - Latest context and credentials - Command/directive updates Machine: Mikes-MacBook-Air.local Timestamp: 2026-03-09 08:14:13 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
384
api/routers/admin_quotes.py
Normal file
384
api/routers/admin_quotes.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""
|
||||
Admin Quote API router for ClaudeTools.
|
||||
|
||||
This module defines all admin REST API endpoints for managing quotes,
|
||||
requiring JWT authentication for access.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.database import get_db
|
||||
from api.middleware.auth import get_current_user
|
||||
from api.schemas.quote import (
|
||||
QuoteAdminResponse,
|
||||
QuoteAdminUpdate,
|
||||
QuoteActivityResponse,
|
||||
QuoteItemResponse,
|
||||
QuoteListItem,
|
||||
QuoteListResponse,
|
||||
QuoteNotificationResponse,
|
||||
QuoteStatsResponse,
|
||||
QuoteStatus,
|
||||
)
|
||||
from api.services import quote_service
|
||||
|
||||
# Create router with authentication required
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=QuoteListResponse,
|
||||
summary="List all quotes",
|
||||
description="Retrieve a paginated list of all quotes with optional filtering",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def list_quotes(
|
||||
skip: int = Query(
|
||||
default=0,
|
||||
ge=0,
|
||||
description="Number of records to skip for pagination"
|
||||
),
|
||||
limit: int = Query(
|
||||
default=100,
|
||||
ge=1,
|
||||
le=1000,
|
||||
description="Maximum number of records to return (max 1000)"
|
||||
),
|
||||
status_filter: Optional[str] = Query(
|
||||
default=None,
|
||||
alias="status",
|
||||
description="Filter by status (draft, submitted, reviewing, approved, rejected, expired)"
|
||||
),
|
||||
search: Optional[str] = Query(
|
||||
default=None,
|
||||
description="Search in company_name, contact_name, contact_email"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
List all quotes with pagination and filtering.
|
||||
|
||||
- **skip**: Number of quotes to skip (default: 0)
|
||||
- **limit**: Maximum number of quotes to return (default: 100, max: 1000)
|
||||
- **status**: Filter by quote status
|
||||
- **search**: Search in company name, contact name, or email
|
||||
|
||||
Returns a list of quotes with pagination metadata.
|
||||
|
||||
**Example Request:**
|
||||
```
|
||||
GET /api/admin/quotes?skip=0&limit=50&status=submitted
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"total": 25,
|
||||
"skip": 0,
|
||||
"limit": 50,
|
||||
"quotes": [
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"access_token": "xYz123...",
|
||||
"status": "submitted",
|
||||
"company_name": "Acme Corporation",
|
||||
"contact_name": "John Doe",
|
||||
"contact_email": "john@acme.com",
|
||||
"employee_count": 25,
|
||||
"monthly_total": "450.00",
|
||||
"setup_total": "500.00",
|
||||
"item_count": 3,
|
||||
"submitted_at": "2024-01-15T14:30:00Z",
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
quotes, total = quote_service.list_quotes(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
status_filter=status_filter,
|
||||
search=search
|
||||
)
|
||||
|
||||
# Build list items with item counts
|
||||
quote_items = []
|
||||
for quote in quotes:
|
||||
quote_items.append(QuoteListItem(
|
||||
id=quote.id,
|
||||
access_token=quote.access_token,
|
||||
status=quote.status,
|
||||
company_name=quote.company_name,
|
||||
contact_name=quote.contact_name,
|
||||
contact_email=quote.contact_email,
|
||||
employee_count=quote.employee_count,
|
||||
monthly_total=quote.monthly_total,
|
||||
setup_total=quote.setup_total,
|
||||
item_count=len(quote.items),
|
||||
submitted_at=quote.submitted_at,
|
||||
created_at=quote.created_at
|
||||
))
|
||||
|
||||
return QuoteListResponse(
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
quotes=quote_items
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/stats",
|
||||
response_model=QuoteStatsResponse,
|
||||
summary="Get quote statistics",
|
||||
description="Get dashboard statistics for quotes",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def get_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get quote statistics for the admin dashboard.
|
||||
|
||||
Returns aggregate statistics including totals, counts by status,
|
||||
and conversion rates.
|
||||
|
||||
**Example Request:**
|
||||
```
|
||||
GET /api/admin/quotes/stats
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"total_quotes": 150,
|
||||
"quotes_by_status": {
|
||||
"draft": 45,
|
||||
"submitted": 60,
|
||||
"reviewing": 15,
|
||||
"approved": 25,
|
||||
"rejected": 3,
|
||||
"expired": 2
|
||||
},
|
||||
"total_monthly_value": "12500.00",
|
||||
"total_setup_value": "8500.00",
|
||||
"quotes_this_month": 28,
|
||||
"quotes_submitted_this_month": 18,
|
||||
"average_monthly_value": "125.00",
|
||||
"conversion_rate": "66.67"
|
||||
}
|
||||
```
|
||||
"""
|
||||
return quote_service.get_quote_stats(db)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{quote_id}",
|
||||
response_model=QuoteAdminResponse,
|
||||
summary="Get quote by ID",
|
||||
description="Retrieve a single quote by its ID with full details",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Quote found and returned",
|
||||
"model": QuoteAdminResponse,
|
||||
},
|
||||
404: {
|
||||
"description": "Quote not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {"detail": "Quote with ID 123e4567-e89b-12d3-a456-426614174000 not found"}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_quote(
|
||||
quote_id: UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get a specific quote by ID with full admin details.
|
||||
|
||||
Returns the quote with all items, activities, and notifications.
|
||||
|
||||
**Example Request:**
|
||||
```
|
||||
GET /api/admin/quotes/123e4567-e89b-12d3-a456-426614174000
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"access_token": "xYz123...",
|
||||
"status": "submitted",
|
||||
"company_name": "Acme Corporation",
|
||||
"contact_name": "John Doe",
|
||||
"contact_email": "john@acme.com",
|
||||
"admin_notes": "Follow up scheduled for next week",
|
||||
"ip_address": "192.168.1.100",
|
||||
"user_agent": "Mozilla/5.0...",
|
||||
"items": [...],
|
||||
"activities": [
|
||||
{
|
||||
"id": "789...",
|
||||
"action": "submitted",
|
||||
"description": "Quote submitted by John Doe (john@acme.com)",
|
||||
"actor": "john@acme.com",
|
||||
"created_at": "2024-01-15T14:30:00Z"
|
||||
}
|
||||
],
|
||||
"notifications": [...]
|
||||
}
|
||||
```
|
||||
"""
|
||||
quote = quote_service.get_quote_by_id(db, quote_id)
|
||||
|
||||
# Build response with all related data
|
||||
items_response = []
|
||||
for item in quote.items:
|
||||
items_response.append(QuoteItemResponse(
|
||||
id=item.id,
|
||||
quote_id=item.quote_id,
|
||||
service_name=item.service_name,
|
||||
service_description=item.service_description,
|
||||
category=item.category,
|
||||
billing_frequency=item.billing_frequency,
|
||||
unit_price=item.unit_price,
|
||||
quantity=item.quantity,
|
||||
setup_fee=item.setup_fee,
|
||||
is_required=item.is_required,
|
||||
sort_order=item.sort_order,
|
||||
line_total=item.line_total,
|
||||
monthly_amount=item.monthly_amount,
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at
|
||||
))
|
||||
|
||||
activities_response = []
|
||||
for activity in quote.activities:
|
||||
activities_response.append(QuoteActivityResponse(
|
||||
id=activity.id,
|
||||
quote_id=activity.quote_id,
|
||||
action=activity.action,
|
||||
description=activity.description,
|
||||
actor=activity.actor,
|
||||
ip_address=activity.ip_address,
|
||||
created_at=activity.created_at
|
||||
))
|
||||
|
||||
notifications_response = []
|
||||
for notification in quote.notifications:
|
||||
notifications_response.append(QuoteNotificationResponse(
|
||||
id=notification.id,
|
||||
quote_id=notification.quote_id,
|
||||
notification_type=notification.notification_type,
|
||||
recipient=notification.recipient,
|
||||
subject=notification.subject,
|
||||
status=notification.status,
|
||||
sent_at=notification.sent_at,
|
||||
error_message=notification.error_message,
|
||||
created_at=notification.created_at
|
||||
))
|
||||
|
||||
return QuoteAdminResponse(
|
||||
id=quote.id,
|
||||
access_token=quote.access_token,
|
||||
status=quote.status,
|
||||
company_name=quote.company_name,
|
||||
contact_name=quote.contact_name,
|
||||
contact_email=quote.contact_email,
|
||||
contact_phone=quote.contact_phone,
|
||||
employee_count=quote.employee_count,
|
||||
notes=quote.notes,
|
||||
admin_notes=quote.admin_notes,
|
||||
monthly_total=quote.monthly_total,
|
||||
setup_total=quote.setup_total,
|
||||
annual_total=quote.annual_total,
|
||||
expires_at=quote.expires_at,
|
||||
submitted_at=quote.submitted_at,
|
||||
ip_address=quote.ip_address,
|
||||
user_agent=quote.user_agent,
|
||||
created_at=quote.created_at,
|
||||
updated_at=quote.updated_at,
|
||||
items=items_response,
|
||||
activities=activities_response,
|
||||
notifications=notifications_response
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{quote_id}",
|
||||
response_model=QuoteAdminResponse,
|
||||
summary="Update quote status/notes",
|
||||
description="Update a quote's status or admin notes",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Quote updated successfully",
|
||||
"model": QuoteAdminResponse,
|
||||
},
|
||||
404: {
|
||||
"description": "Quote not found",
|
||||
},
|
||||
},
|
||||
)
|
||||
def update_quote(
|
||||
quote_id: UUID,
|
||||
update_data: QuoteAdminUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update a quote's status or admin notes.
|
||||
|
||||
Admins can change the quote status (e.g., from submitted to reviewing
|
||||
or approved) and add internal notes.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
PUT /api/admin/quotes/123e4567-e89b-12d3-a456-426614174000
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "reviewing",
|
||||
"admin_notes": "Assigned to sales team. Follow up scheduled for Monday."
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"status": "reviewing",
|
||||
"admin_notes": "Assigned to sales team. Follow up scheduled for Monday.",
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
# Get admin username from token
|
||||
admin_user = current_user.get("sub", "admin")
|
||||
|
||||
quote_service.update_quote_status(
|
||||
db=db,
|
||||
quote_id=quote_id,
|
||||
update_data=update_data,
|
||||
admin_user=admin_user
|
||||
)
|
||||
|
||||
return get_quote(quote_id, db, current_user)
|
||||
519
api/routers/quotes.py
Normal file
519
api/routers/quotes.py
Normal file
@@ -0,0 +1,519 @@
|
||||
"""
|
||||
Public Quote API router for ClaudeTools.
|
||||
|
||||
This module defines all public REST API endpoints for the MSP Quote Wizard,
|
||||
allowing prospects to create, view, and submit quotes without authentication.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.database import get_db
|
||||
from api.schemas.quote import (
|
||||
QuoteCreate,
|
||||
QuoteCreatedResponse,
|
||||
QuoteItemCreate,
|
||||
QuoteResponse,
|
||||
QuoteItemResponse,
|
||||
QuoteSubmit,
|
||||
QuoteUpdate,
|
||||
)
|
||||
from api.services import quote_service
|
||||
|
||||
# Create router (no authentication required for public endpoints)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def get_client_ip(request: Request) -> Optional[str]:
|
||||
"""Extract client IP from request, handling proxies."""
|
||||
forwarded = request.headers.get("X-Forwarded-For")
|
||||
if forwarded:
|
||||
return forwarded.split(",")[0].strip()
|
||||
return request.client.host if request.client else None
|
||||
|
||||
|
||||
def get_user_agent(request: Request) -> Optional[str]:
|
||||
"""Extract user agent from request."""
|
||||
return request.headers.get("User-Agent")
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=QuoteCreatedResponse,
|
||||
summary="Create new quote draft",
|
||||
description="Create a new quote draft. Returns an access token for future access.",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={
|
||||
201: {
|
||||
"description": "Quote created successfully",
|
||||
"model": QuoteCreatedResponse,
|
||||
},
|
||||
500: {
|
||||
"description": "Server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {"detail": "Failed to create quote"}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
def create_quote(
|
||||
quote_data: QuoteCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Create a new quote draft.
|
||||
|
||||
This endpoint does not require authentication. A unique access token is
|
||||
generated for the quote which can be used to access it later.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
POST /api/quotes
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"employee_count": 25,
|
||||
"notes": "Looking for complete managed services package"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"access_token": "xYz123abc456def789ghi012jkl345mno678pqr901stu",
|
||||
"status": "draft",
|
||||
"message": "Quote created successfully. Use the access_token to access your quote."
|
||||
}
|
||||
```
|
||||
"""
|
||||
ip_address = get_client_ip(request)
|
||||
user_agent = get_user_agent(request)
|
||||
|
||||
quote = quote_service.create_quote(
|
||||
db=db,
|
||||
quote_data=quote_data,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
return QuoteCreatedResponse(
|
||||
id=quote.id,
|
||||
access_token=quote.access_token,
|
||||
status=quote.status,
|
||||
message="Quote created successfully. Use the access_token to access your quote."
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{access_token}",
|
||||
response_model=QuoteResponse,
|
||||
summary="Get quote by access token",
|
||||
description="Retrieve a quote by its access token",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Quote found and returned",
|
||||
"model": QuoteResponse,
|
||||
},
|
||||
404: {
|
||||
"description": "Quote not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {"detail": "Quote not found"}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_quote(
|
||||
access_token: str,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get a quote by its access token.
|
||||
|
||||
Returns the quote with all its items. This is the public endpoint
|
||||
for viewing a quote.
|
||||
|
||||
**Example Request:**
|
||||
```
|
||||
GET /api/quotes/xYz123abc456def789ghi012jkl345mno678pqr901stu
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"access_token": "xYz123abc456def789ghi012jkl345mno678pqr901stu",
|
||||
"status": "draft",
|
||||
"company_name": null,
|
||||
"contact_name": null,
|
||||
"contact_email": null,
|
||||
"employee_count": 25,
|
||||
"monthly_total": "450.00",
|
||||
"setup_total": "500.00",
|
||||
"annual_total": "5900.00",
|
||||
"items": [
|
||||
{
|
||||
"id": "456e7890-e89b-12d3-a456-426614174001",
|
||||
"service_name": "Managed Endpoint Protection",
|
||||
"category": "security",
|
||||
"unit_price": "15.00",
|
||||
"quantity": 25,
|
||||
"billing_frequency": "monthly",
|
||||
"line_total": "375.00",
|
||||
"monthly_amount": "375.00"
|
||||
}
|
||||
],
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
"""
|
||||
quote = quote_service.get_quote_by_token(db, access_token)
|
||||
|
||||
# Build response with calculated fields for items
|
||||
items_response = []
|
||||
for item in quote.items:
|
||||
item_dict = QuoteItemResponse(
|
||||
id=item.id,
|
||||
quote_id=item.quote_id,
|
||||
service_name=item.service_name,
|
||||
service_description=item.service_description,
|
||||
category=item.category,
|
||||
billing_frequency=item.billing_frequency,
|
||||
unit_price=item.unit_price,
|
||||
quantity=item.quantity,
|
||||
setup_fee=item.setup_fee,
|
||||
is_required=item.is_required,
|
||||
sort_order=item.sort_order,
|
||||
line_total=item.line_total,
|
||||
monthly_amount=item.monthly_amount,
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at
|
||||
)
|
||||
items_response.append(item_dict)
|
||||
|
||||
return QuoteResponse(
|
||||
id=quote.id,
|
||||
access_token=quote.access_token,
|
||||
status=quote.status,
|
||||
company_name=quote.company_name,
|
||||
contact_name=quote.contact_name,
|
||||
contact_email=quote.contact_email,
|
||||
contact_phone=quote.contact_phone,
|
||||
employee_count=quote.employee_count,
|
||||
notes=quote.notes,
|
||||
monthly_total=quote.monthly_total,
|
||||
setup_total=quote.setup_total,
|
||||
annual_total=quote.annual_total,
|
||||
expires_at=quote.expires_at,
|
||||
submitted_at=quote.submitted_at,
|
||||
created_at=quote.created_at,
|
||||
updated_at=quote.updated_at,
|
||||
items=items_response
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{access_token}",
|
||||
response_model=QuoteResponse,
|
||||
summary="Update quote",
|
||||
description="Update a quote's details and/or items",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Quote updated successfully",
|
||||
"model": QuoteResponse,
|
||||
},
|
||||
400: {
|
||||
"description": "Quote cannot be modified (not a draft)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {"detail": "Cannot update quote with status 'submitted'. Only drafts can be modified."}
|
||||
}
|
||||
},
|
||||
},
|
||||
404: {
|
||||
"description": "Quote not found",
|
||||
},
|
||||
},
|
||||
)
|
||||
def update_quote(
|
||||
access_token: str,
|
||||
quote_data: QuoteUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update a quote.
|
||||
|
||||
Updates quote details and/or replaces all items. Only draft quotes
|
||||
can be modified.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
PUT /api/quotes/xYz123abc456def789ghi012jkl345mno678pqr901stu
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"employee_count": 30,
|
||||
"items": [
|
||||
{
|
||||
"service_name": "Managed Endpoint Protection",
|
||||
"category": "security",
|
||||
"unit_price": "15.00",
|
||||
"quantity": 30,
|
||||
"billing_frequency": "monthly"
|
||||
},
|
||||
{
|
||||
"service_name": "Cloud Backup",
|
||||
"category": "backup",
|
||||
"unit_price": "5.00",
|
||||
"quantity": 30,
|
||||
"billing_frequency": "monthly",
|
||||
"setup_fee": "250.00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
ip_address = get_client_ip(request)
|
||||
|
||||
quote = quote_service.update_quote(
|
||||
db=db,
|
||||
access_token=access_token,
|
||||
quote_data=quote_data,
|
||||
ip_address=ip_address
|
||||
)
|
||||
|
||||
return get_quote(access_token, db)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{access_token}/items",
|
||||
response_model=QuoteResponse,
|
||||
summary="Add item to quote",
|
||||
description="Add a single item to the quote",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={
|
||||
201: {
|
||||
"description": "Item added successfully",
|
||||
"model": QuoteResponse,
|
||||
},
|
||||
400: {
|
||||
"description": "Quote cannot be modified",
|
||||
},
|
||||
404: {
|
||||
"description": "Quote not found",
|
||||
},
|
||||
},
|
||||
)
|
||||
def add_item(
|
||||
access_token: str,
|
||||
item_data: QuoteItemCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Add a single item to a quote.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
POST /api/quotes/xYz123abc456def789ghi012jkl345mno678pqr901stu/items
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"service_name": "24/7 Help Desk Support",
|
||||
"category": "support",
|
||||
"unit_price": "50.00",
|
||||
"quantity": 1,
|
||||
"billing_frequency": "monthly"
|
||||
}
|
||||
```
|
||||
"""
|
||||
ip_address = get_client_ip(request)
|
||||
|
||||
quote_service.add_item_to_quote(
|
||||
db=db,
|
||||
access_token=access_token,
|
||||
item_data=item_data,
|
||||
ip_address=ip_address
|
||||
)
|
||||
|
||||
return get_quote(access_token, db)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{access_token}/items/{item_id}",
|
||||
response_model=QuoteResponse,
|
||||
summary="Remove item from quote",
|
||||
description="Remove an item from the quote",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Item removed successfully",
|
||||
"model": QuoteResponse,
|
||||
},
|
||||
400: {
|
||||
"description": "Quote cannot be modified or item is required",
|
||||
},
|
||||
404: {
|
||||
"description": "Quote or item not found",
|
||||
},
|
||||
},
|
||||
)
|
||||
def remove_item(
|
||||
access_token: str,
|
||||
item_id: UUID,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Remove an item from a quote.
|
||||
|
||||
Required items cannot be removed.
|
||||
|
||||
**Example Request:**
|
||||
```
|
||||
DELETE /api/quotes/xYz123abc456def789ghi012jkl345mno678pqr901stu/items/456e7890-e89b-12d3-a456-426614174001
|
||||
```
|
||||
"""
|
||||
ip_address = get_client_ip(request)
|
||||
|
||||
quote_service.remove_item_from_quote(
|
||||
db=db,
|
||||
access_token=access_token,
|
||||
item_id=item_id,
|
||||
ip_address=ip_address
|
||||
)
|
||||
|
||||
return get_quote(access_token, db)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{access_token}/submit",
|
||||
response_model=QuoteResponse,
|
||||
summary="Submit quote",
|
||||
description="Submit the quote with contact information",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Quote submitted successfully",
|
||||
"model": QuoteResponse,
|
||||
},
|
||||
400: {
|
||||
"description": "Quote cannot be submitted (not a draft or no items)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"not_draft": {
|
||||
"value": {"detail": "Cannot submit quote with status 'submitted'. Only drafts can be submitted."}
|
||||
},
|
||||
"no_items": {
|
||||
"value": {"detail": "Cannot submit quote without any items. Please add at least one service."}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
404: {
|
||||
"description": "Quote not found",
|
||||
},
|
||||
422: {
|
||||
"description": "Validation error - missing required fields",
|
||||
},
|
||||
},
|
||||
)
|
||||
def submit_quote(
|
||||
access_token: str,
|
||||
submit_data: QuoteSubmit,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Submit a quote with contact information.
|
||||
|
||||
This finalizes the quote and sends it for review. Contact information
|
||||
is required at this stage.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
POST /api/quotes/xYz123abc456def789ghi012jkl345mno678pqr901stu/submit
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"company_name": "Acme Corporation",
|
||||
"contact_name": "John Doe",
|
||||
"contact_email": "john.doe@acme.com",
|
||||
"contact_phone": "555-123-4567",
|
||||
"notes": "Please contact me to discuss implementation timeline."
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"access_token": "xYz123abc456def789ghi012jkl345mno678pqr901stu",
|
||||
"status": "submitted",
|
||||
"company_name": "Acme Corporation",
|
||||
"contact_name": "John Doe",
|
||||
"contact_email": "john.doe@acme.com",
|
||||
"submitted_at": "2024-01-15T14:30:00Z",
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
ip_address = get_client_ip(request)
|
||||
|
||||
quote_service.submit_quote(
|
||||
db=db,
|
||||
access_token=access_token,
|
||||
submit_data=submit_data,
|
||||
ip_address=ip_address
|
||||
)
|
||||
|
||||
return get_quote(access_token, db)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{access_token}/pdf",
|
||||
summary="Get quote PDF (placeholder)",
|
||||
description="Generate and return a PDF version of the quote",
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
responses={
|
||||
501: {
|
||||
"description": "PDF generation not yet implemented",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {"detail": "PDF generation is not yet implemented"}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_quote_pdf(
|
||||
access_token: str,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Generate a PDF version of the quote.
|
||||
|
||||
**Note:** This endpoint is a placeholder. PDF generation will be
|
||||
implemented in a future update.
|
||||
"""
|
||||
# Verify quote exists
|
||||
quote_service.get_quote_by_token(db, access_token)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="PDF generation is not yet implemented"
|
||||
)
|
||||
Reference in New Issue
Block a user