"""Quotes: the customer's cart, approved at once when it can be (app/quote_review.py).""" import hashlib import json from decimal import Decimal from uuid import UUID, uuid4 from fastapi import APIRouter, Depends, HTTPException from psycopg.types.json import Jsonb from .. import payments from ..core import db from ..core.auth import owner from ..core.models import QuoteRequest from ..core.pricing import price from .. import quote_review from ..runtime import freight, quote_view, require_delivery_available, upload_row from ..scanning import require_clean router = APIRouter() @router.post('/api/quotes') def create_quote(body: QuoteRequest, session_id=Depends(owner)): for item in body.items: if abs(item.metres*100-item.production.height_cm) > Decimal('0.02'): raise HTTPException(422, 'Quoted metres do not match the submitted layout height') require_delivery_available(body.freight.service) draft = body.model_dump(mode='json', exclude={'request_key'}) digest = hashlib.sha256(json.dumps(draft, sort_keys=True).encode()).hexdigest() reason = quote_review.review_reason(draft) if reason is not None: # Waits for review: check the delivery now, priced at approval. try: priced = [price(item.mode, str(item.metres), item.grade) for item in body.items] freight.quote(body.freight.service, body.freight.postal_code, sum(Decimal(i['billed_metres']) for i in priced), sum(i['total_cents'] for i in priced)) except ValueError as exc: raise HTTPException(422, str(exc)) with db.connect() as c: for item in body.items: for uid in item.uploads: row = upload_row(c, uid, session_id) if not row['complete']: raise HTTPException(409, 'Complete every upload before requesting a quote') require_clean(row) uid = uuid4() c.execute('INSERT INTO dtf_local.quotes(id,owner,request_key,request_hash,draft) VALUES(%s,%s,%s,%s,%s) ON CONFLICT(owner,request_key) DO NOTHING', (uid, session_id, body.request_key, digest, Jsonb(draft))) row = c.execute('SELECT * FROM dtf_local.quotes WHERE owner=%s AND request_key=%s FOR UPDATE', (session_id, body.request_key)).fetchone() if row['request_hash'] != digest: raise HTTPException(409, 'Request key already used for a different cart') if row['id'] == uid and reason is None: quote_review.approve(c, row, body.items, quote_review.AUTO) return {'id': row['id'], 'status': 'approved'} if row['id'] == uid: # An operator reviews it first; the files wait for that review. payments.hold_uploads(c, payments.quote_uploads(draft), payments.REVIEW_HOLD) return {'id': row['id'], 'status': 'approved' if row['approved'] else 'pending_review'} @router.get('/api/quotes/{uid}') def get_quote(uid: UUID, session_id=Depends(owner)): with db.connect() as c: row = c.execute('SELECT * FROM dtf_local.quotes WHERE id=%s AND owner=%s', (uid,session_id)).fetchone() if not row: raise HTTPException(404, 'Quote not found') return quote_view(c, row)