Files
dtf-system/app/api/quotes.py
Cauê Faleiros 37715ef223
All checks were successful
Build and deploy / Validate source (push) Successful in 1m23s
Build and deploy / Integration suite on a real stack (push) Successful in 3m13s
Build and deploy / Secret scan and release gate (push) Successful in 10s
Build and deploy / Publish images (push) Successful in 1m25s
feat: the server checks the grade against the files before approving
The price depends on the grade, which the browser worked out and the API
took on trust. Before a cart is approved at checkout the API now recomputes
it from the uploaded files by the Site's own rules: the pixel size in a
PNG, JPG or WebP header across the printed width (rotation included), and
the area-weighted DPI of the images placed in a PDF of up to 150 MB, 300
for vectors. Sheets take the worst grade, artworks the average. A claim more
than 2 points above the file's grade, or a discount on a file the server
cannot grade, waits for an operator, with the reason on the Kanban.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:24:37 -03:00

73 lines
3.6 KiB
Python

"""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 ..grade_check import Files, mismatch
from ..runtime import freight, quote_view, require_delivery_available, storage, 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:
rows = {}
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)
rows[str(uid)] = row
# The grade sets the price and came from the browser: before a cart is
# approved at checkout, the server works it out from the files.
note = mismatch(Files(storage), draft['items'], rows) if reason is None else None
reason = reason or note
uid = uuid4()
c.execute('INSERT INTO dtf_local.quotes(id,owner,request_key,request_hash,draft,review_note) VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT(owner,request_key) DO NOTHING',
(uid, session_id, body.request_key, digest, Jsonb(draft), note))
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)