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:
2026-03-10 19:59:08 -07:00
parent a1a19f8c00
commit fa15b03180
169 changed files with 879909 additions and 1243 deletions

View File

@@ -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

View File

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