Files
claudetools/api/routers/quotes.py
azcomputerguru a1a19f8c00 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>
2026-03-09 08:14:13 -07:00

520 lines
14 KiB
Python

"""
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"
)