sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,7 @@ def list_quotes(
|
||||
status_filter: Optional[str] = Query(
|
||||
default=None,
|
||||
alias="status",
|
||||
description="Filter by status (draft, submitted, reviewing, approved, rejected, expired)"
|
||||
description="Filter by status (draft, submitted, viewed, followed_up, converted, expired)"
|
||||
),
|
||||
search: Optional[str] = Query(
|
||||
default=None,
|
||||
@@ -166,9 +166,9 @@ def get_stats(
|
||||
"quotes_by_status": {
|
||||
"draft": 45,
|
||||
"submitted": 60,
|
||||
"reviewing": 15,
|
||||
"approved": 25,
|
||||
"rejected": 3,
|
||||
"viewed": 15,
|
||||
"followed_up": 10,
|
||||
"converted": 25,
|
||||
"expired": 2
|
||||
},
|
||||
"total_monthly_value": "12500.00",
|
||||
@@ -229,7 +229,6 @@ def get_quote(
|
||||
"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": [...],
|
||||
@@ -254,19 +253,19 @@ def get_quote(
|
||||
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,
|
||||
product_code=item.product_code,
|
||||
product_name=item.product_name,
|
||||
description=item.description,
|
||||
quantity=item.quantity,
|
||||
setup_fee=item.setup_fee,
|
||||
is_required=item.is_required,
|
||||
sort_order=item.sort_order,
|
||||
unit_price=item.unit_price,
|
||||
setup_price=item.setup_price,
|
||||
billing_frequency=item.billing_frequency,
|
||||
tier=item.tier,
|
||||
is_recommended=item.is_recommended,
|
||||
line_total=item.line_total,
|
||||
monthly_amount=item.monthly_amount,
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at
|
||||
))
|
||||
|
||||
activities_response = []
|
||||
@@ -275,8 +274,8 @@ def get_quote(
|
||||
id=activity.id,
|
||||
quote_id=activity.quote_id,
|
||||
action=activity.action,
|
||||
description=activity.description,
|
||||
actor=activity.actor,
|
||||
step_name=activity.step_name,
|
||||
details=activity.details,
|
||||
ip_address=activity.ip_address,
|
||||
created_at=activity.created_at
|
||||
))
|
||||
@@ -290,6 +289,8 @@ def get_quote(
|
||||
recipient=notification.recipient,
|
||||
subject=notification.subject,
|
||||
status=notification.status,
|
||||
attempts=notification.attempts,
|
||||
last_attempt_at=notification.last_attempt_at,
|
||||
sent_at=notification.sent_at,
|
||||
error_message=notification.error_message,
|
||||
created_at=notification.created_at
|
||||
@@ -304,11 +305,8 @@ def get_quote(
|
||||
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,
|
||||
@@ -346,8 +344,8 @@ def update_quote(
|
||||
"""
|
||||
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.
|
||||
Admins can change the quote status (e.g., from submitted to viewed
|
||||
or converted) and update expiration.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
@@ -356,8 +354,7 @@ def update_quote(
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "reviewing",
|
||||
"admin_notes": "Assigned to sales team. Follow up scheduled for Monday."
|
||||
"status": "viewed"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -365,8 +362,7 @@ def update_quote(
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"status": "reviewing",
|
||||
"admin_notes": "Assigned to sales team. Follow up scheduled for Monday.",
|
||||
"status": "viewed",
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -382,3 +378,47 @@ def update_quote(
|
||||
)
|
||||
|
||||
return get_quote(quote_id, db, current_user)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{quote_id}/sync-syncro",
|
||||
summary="Sync quote to SyncroRMM",
|
||||
description="Create or update a lead in SyncroRMM from a submitted quote",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Sync result",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"synced": True,
|
||||
"is_existing_customer": False,
|
||||
"syncro_lead_id": "12345",
|
||||
"error": None,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
404: {"description": "Quote not found"},
|
||||
},
|
||||
)
|
||||
async def sync_quote_to_syncro(
|
||||
quote_id: UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Manually trigger a SyncroRMM sync for a quote.
|
||||
|
||||
Checks for an existing customer in Syncro and creates a lead with
|
||||
the quote details. The quote must have a contact email to sync.
|
||||
|
||||
**Example Request:**
|
||||
```
|
||||
POST /api/admin/quotes/123e4567-e89b-12d3-a456-426614174000/sync-syncro
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
"""
|
||||
quote = quote_service.get_quote_by_id(db, quote_id)
|
||||
result = await quote_service.sync_quote_to_syncro(db, quote)
|
||||
return result
|
||||
|
||||
@@ -78,8 +78,7 @@ def create_quote(
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"employee_count": 25,
|
||||
"notes": "Looking for complete managed services package"
|
||||
"employee_count": 25
|
||||
}
|
||||
```
|
||||
|
||||
@@ -159,7 +158,6 @@ def get_quote(
|
||||
"employee_count": 25,
|
||||
"monthly_total": "450.00",
|
||||
"setup_total": "500.00",
|
||||
"annual_total": "5900.00",
|
||||
"items": [
|
||||
{
|
||||
"id": "456e7890-e89b-12d3-a456-426614174001",
|
||||
@@ -185,19 +183,19 @@ def get_quote(
|
||||
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,
|
||||
product_code=item.product_code,
|
||||
product_name=item.product_name,
|
||||
description=item.description,
|
||||
quantity=item.quantity,
|
||||
setup_fee=item.setup_fee,
|
||||
is_required=item.is_required,
|
||||
sort_order=item.sort_order,
|
||||
unit_price=item.unit_price,
|
||||
setup_price=item.setup_price,
|
||||
billing_frequency=item.billing_frequency,
|
||||
tier=item.tier,
|
||||
is_recommended=item.is_recommended,
|
||||
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)
|
||||
|
||||
@@ -210,10 +208,8 @@ def get_quote(
|
||||
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,
|
||||
@@ -432,7 +428,7 @@ def remove_item(
|
||||
},
|
||||
},
|
||||
)
|
||||
def submit_quote(
|
||||
async def submit_quote_endpoint(
|
||||
access_token: str,
|
||||
submit_data: QuoteSubmit,
|
||||
request: Request,
|
||||
@@ -442,7 +438,7 @@ def submit_quote(
|
||||
Submit a quote with contact information.
|
||||
|
||||
This finalizes the quote and sends it for review. Contact information
|
||||
is required at this stage.
|
||||
is required at this stage. An email notification is sent to the admin.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
@@ -453,8 +449,7 @@ def submit_quote(
|
||||
"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."
|
||||
"contact_phone": "555-123-4567"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -472,15 +467,62 @@ def submit_quote(
|
||||
}
|
||||
```
|
||||
"""
|
||||
import logging
|
||||
from api.config import get_settings
|
||||
from api.services.email_service import send_email, build_quote_notification_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ip_address = get_client_ip(request)
|
||||
|
||||
quote_service.submit_quote(
|
||||
quote = quote_service.submit_quote(
|
||||
db=db,
|
||||
access_token=access_token,
|
||||
submit_data=submit_data,
|
||||
ip_address=ip_address
|
||||
)
|
||||
|
||||
# Send email notification (non-blocking, don't fail the request if email fails)
|
||||
try:
|
||||
settings = get_settings()
|
||||
items_data = [
|
||||
{
|
||||
"service_name": item.product_name,
|
||||
"billing_frequency": item.billing_frequency,
|
||||
"unit_price": str(item.unit_price),
|
||||
"quantity": item.quantity,
|
||||
}
|
||||
for item in quote.items
|
||||
]
|
||||
|
||||
html = build_quote_notification_html(
|
||||
company_name=submit_data.company_name,
|
||||
contact_name=submit_data.contact_name,
|
||||
contact_email=submit_data.contact_email,
|
||||
contact_phone=submit_data.contact_phone,
|
||||
monthly_total=str(quote.monthly_total),
|
||||
setup_total=str(quote.setup_total),
|
||||
items=items_data,
|
||||
notes=submit_data.notes,
|
||||
)
|
||||
|
||||
sent = await send_email(
|
||||
to_email=settings.ADMIN_NOTIFICATION_EMAIL,
|
||||
subject=f"New Quote Submission: {submit_data.company_name} - ${quote.monthly_total}/mo",
|
||||
body_html=html,
|
||||
)
|
||||
|
||||
# Update notification record status
|
||||
if quote.notifications:
|
||||
notification = quote.notifications[-1]
|
||||
notification.status = "sent" if sent else "failed"
|
||||
if not sent:
|
||||
notification.error_message = "Graph API send failed"
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send quote notification email: {e}")
|
||||
# Don't fail the submission - email is best-effort
|
||||
|
||||
return get_quote(access_token, db)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user