from typing import Dict, Any, Optional, List from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_, or_ from app.models.quotation import Quotation, QuotationItem from app.models.customer import Customer from app.models.user import Product from app.ai.router import get_ai_router from datetime import datetime import logging import json logger = logging.getLogger(__name__) class QuotationService: def __init__(self, db: AsyncSession): self.db = db async def create_quotation(self, user_id: str, data: Dict[str, Any]) -> Dict: items_data = data.pop("items", []) if data.get("customer_id"): cust_result = await self.db.execute( select(Customer).where( and_(Customer.id == data["customer_id"], Customer.user_id == user_id) ) ) if not cust_result.scalar_one_or_none(): raise ValueError("Customer not found") q = Quotation(user_id=user_id, **data) self.db.add(q) await self.db.flush() total = 0 for item_data in items_data: item_total = item_data.get("quantity", 0) * item_data.get("unit_price", 0) item = QuotationItem( quotation_id=q.id, product_name=item_data["product_name"], description=item_data.get("description"), quantity=item_data["quantity"], unit_price=item_data["unit_price"], total_price=item_total, unit=item_data.get("unit", "pcs"), ) self.db.add(item) total += item_total q.subtotal = total q.total = total - (data.get("discount", 0)) + (data.get("shipping", 0)) await self.db.flush() return await self._to_dict(q) async def get_quotation(self, user_id: str, quotation_id: str) -> Optional[Dict]: result = await self.db.execute( select(Quotation).where( and_(Quotation.id == quotation_id, Quotation.user_id == user_id) ) ) q = result.scalar_one_or_none() return await self._to_dict(q) if q else None async def list_quotations(self, user_id: str, page: int = 1, size: int = 20) -> Dict[str, Any]: from sqlalchemy import func query = select(Quotation).where(Quotation.user_id == user_id).order_by(Quotation.created_at.desc()).offset((page - 1) * size).limit(size) count_query = select(func.count()).select_from(Quotation).where(Quotation.user_id == user_id) total = await self.db.execute(count_query) result = await self.db.execute(query) quotations = result.scalars().all() items = [] for q in quotations: items.append(await self._to_dict(q)) return {"items": items, "total": total.scalar(), "page": page, "size": size} async def update_status(self, user_id: str, quotation_id: str, status: str) -> Optional[Dict]: result = await self.db.execute( select(Quotation).where( and_(Quotation.id == quotation_id, Quotation.user_id == user_id) ) ) q = result.scalar_one_or_none() if not q: return None q.status = status if status == "sent": q.sent_at = datetime.utcnow() await self.db.flush() return await self._to_dict(q) async def generate_from_inquiry( self, user_id: str, inquiry_text: str, customer_id: Optional[str] = None, ) -> Dict[str, Any]: ai = get_ai_router() schema = { "type": "object", "properties": { "product_requests": { "type": "array", "items": { "type": "object", "properties": { "product_name": {"type": "string"}, "quantity": {"type": "integer"}, "unit": {"type": "string"}, "specifications": {"type": "string"}, "target_price": {"type": "string"}, }, }, }, "payment_terms": {"type": "string"}, "delivery_terms": {"type": "string"}, "urgency": {"type": "string"}, }, } extract_result = await ai.extract(inquiry_text, schema) extracted = extract_result.get("data", {}) product_requests = extracted.get("product_requests", []) if not product_requests: schema_simple = { "type": "object", "properties": { "product_name": {"type": "string"}, "quantity": {"type": "integer"}, "specifications": {"type": "string"}, }, } extract_result = await ai.extract(inquiry_text, schema_simple) extracted = extract_result.get("data", {}) if extracted.get("product_name"): product_requests = [{ "product_name": extracted["product_name"], "quantity": extracted.get("quantity", 1), "unit": "pcs", "specifications": extracted.get("specifications", ""), }] product_result = await self.db.execute( select(Product).where( and_( Product.user_id == user_id, Product.is_active == True, ) ) ) user_products = product_result.scalars().all() matched_products = [] for req in product_requests: req_name = req.get("product_name", "").lower() best_match = None best_score = 0 for p in user_products: score = 0 p_name = (p.name or "").lower() p_name_en = (p.name_en or "").lower() if req_name in p_name or p_name in req_name: score += 3 if req_name in p_name_en or p_name_en in req_name: score += 2 keywords = p.keywords or [] for kw in keywords: if isinstance(kw, str) and kw.lower() in req_name: score += 1 if score > best_score: best_score = score best_match = p if best_match and best_score > 0: unit_price = float(best_match.price) if best_match.price else 0 quantity = req.get("quantity", 1) matched_products.append({ "product_id": str(best_match.id), "product_name": best_match.name, "description": best_match.description_en or best_match.description, "quantity": quantity, "unit_price": unit_price, "total_price": unit_price * quantity, "unit": req.get("unit", "pcs"), "match_score": best_score, }) else: matched_products.append({ "product_id": None, "product_name": req.get("product_name", "Unknown"), "description": req.get("specifications", ""), "quantity": req.get("quantity", 1), "unit_price": 0, "total_price": 0, "unit": req.get("unit", "pcs"), "match_score": 0, }) subtotal = sum(p["total_price"] for p in matched_products) total = subtotal suggested_quotation = { "title": f"Quotation - {', '.join(p['product_name'] for p in matched_products[:3])}", "items": matched_products, "subtotal": subtotal, "total": total, "payment_terms": extracted.get("payment_terms", "T/T"), "delivery_terms": extracted.get("delivery_terms", "FOB"), "lead_time": "15-20 days" if extracted.get("urgency") != "urgent" else "7-10 days", "notes": f"Generated from customer inquiry: {inquiry_text[:100]}..." if len(inquiry_text) > 100 else f"Generated from customer inquiry: {inquiry_text}", "extracted_data": extracted, "matched_count": len([p for p in matched_products if p["product_id"]]), "unmatched_count": len([p for p in matched_products if not p["product_id"]]), } return suggested_quotation async def generate_quotation_text(self, q: Quotation) -> str: items_result = await self.db.execute( select(QuotationItem).where(QuotationItem.quotation_id == q.id) ) items = items_result.scalars().all() lines = [f"QUOTATION", f"", f"Date: {datetime.utcnow().strftime('%Y-%m-%d')}"] if q.valid_until: lines.append(f"Valid until: {q.valid_until}") lines.append(f"") lines.append(f"{'Item':<30} {'Qty':<10} {'Unit Price':<15} {'Total':<15}") lines.append("-" * 70) for item in items: lines.append(f"{item.product_name:<30} {item.quantity:<10} ${item.unit_price:<12.2f} ${item.total_price:<10.2f}") lines.append("-" * 70) if q.subtotal: lines.append(f"{'Subtotal':>55} ${q.subtotal:<10.2f}") if q.discount: lines.append(f"{'Discount':>55} -${q.discount:<9.2f}") if q.shipping: lines.append(f"{'Shipping':>55} ${q.shipping:<10.2f}") lines.append(f"{'TOTAL':>55} ${q.total or q.subtotal or 0:<10.2f}") lines.append(f"") if q.payment_terms: lines.append(f"Payment: {q.payment_terms}") if q.delivery_terms: lines.append(f"Delivery: {q.delivery_terms}") if q.lead_time: lines.append(f"Lead time: {q.lead_time}") if q.notes: lines.append(f"") lines.append(f"Notes: {q.notes}") return "\n".join(lines) async def _to_dict(self, q: Quotation) -> Dict: items_result = await self.db.execute( select(QuotationItem).where(QuotationItem.quotation_id == q.id) ) items = items_result.scalars().all() return { "id": str(q.id), "customer_id": str(q.customer_id) if q.customer_id else None, "title": q.title, "status": q.status, "currency": q.currency, "exchange_rate": q.exchange_rate, "payment_terms": q.payment_terms, "delivery_terms": q.delivery_terms, "lead_time": q.lead_time, "valid_until": q.valid_until, "subtotal": q.subtotal, "discount": q.discount, "shipping": q.shipping, "total": q.total, "notes": q.notes, "items": [ { "product_name": i.product_name, "description": i.description, "quantity": i.quantity, "unit_price": i.unit_price, "total_price": i.total_price, "unit": i.unit, } for i in items ], "text": await self.generate_quotation_text(q), "sent_at": q.sent_at.isoformat() if q.sent_at else None, "created_at": q.created_at.isoformat() if q.created_at else None, }