feat: approve priced carts at checkout so customers can pay at once
All checks were successful
Build and deploy / Validate source (push) Successful in 8s
Build and deploy / Integration suite on a real stack (push) Successful in 2m22s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m34s

Every quote waited for an operator before it could be paid, so an order
placed at night waited for the morning. A cart the Site priced is now
approved when the quote is created, through the same server pricing the
operator's approval uses (app/quote_review.py). Orders above
QUOTE_AUTO_MAX_METRES (50 m) and items claiming a discount on art the Site
could not analyse still wait for review; the Kanban shows which quotes were
approved automatically and why the others wait.

The grade is still computed in the browser (roadmap 3.2, 3.9), so the
discount remains a customer-supplied value until the server computes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-28 11:02:56 -03:00
parent 9bea187ea4
commit 275ebf72c4
15 changed files with 200 additions and 51 deletions

View File

@@ -8,17 +8,16 @@ from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from fastapi.responses import RedirectResponse
from psycopg.types.json import Jsonb
from ..core import db
from ..core.auth import (COOKIE_SECURE, DUMMY_PASSWORD_HASH, audit, client_ip, operator,
login_failed, password_matches, throttle)
from ..core.models import Move, OperatorLogin, Resolution, Review
from ..core.pricing import price
from ..printjobs import queue as queue_print_files
from .. import quote_review
from .. import tiny
from ..runtime import (BACK, BOARD_FINISHED_LIMIT, BOARD_QUOTE_LIMIT, ENVIRONMENT, STATES, TRANSITIONS, payment,
enqueue, freight, quote_view, storage, upload_row)
enqueue, quote_view, storage)
from ..scanning import require_clean
router = APIRouter()
@@ -197,31 +196,7 @@ def approve(uid: UUID, body: Review, user=Depends(operator)):
row = c.execute('SELECT * FROM dtf_local.quotes WHERE id=%s FOR UPDATE', (uid,)).fetchone()
if not row:
raise HTTPException(404, 'Quote not found')
if row['approved']:
raise HTTPException(409, 'Approved quotes are immutable; request a new quote')
draft = row['draft']
if any(item.get('production', {}).get('version') != 2 for item in draft['items']):
raise HTTPException(409, 'Quote uses an obsolete production layout; customer must request a new quote')
if len(body.items) != len(draft['items']):
raise HTTPException(422, 'Review must cover every item')
items = []
for item, original in zip(body.items, draft['items']):
if item.mode != original['mode'] or list(map(str,item.uploads)) != original['uploads']:
raise HTTPException(422, 'Product mode and attached files cannot change during review')
if item.production.model_dump(mode='json') != original['production'] or item.quality_status != original['quality_status'] or item.quality_acknowledged != original['quality_acknowledged']:
raise HTTPException(422, 'Production instructions and customer acknowledgement cannot change during commercial review')
for upload_id in item.uploads:
require_clean(upload_row(c, upload_id, row['owner']))
items.append({**price(item.mode, str(item.metres), item.grade),
'uploads': original['uploads'], 'production': original['production'],
'quality_status': original['quality_status'],
'quality_acknowledged': original['quality_acknowledged']})
quoted_freight = freight.quote(**draft['freight'])
approved = {'customer': draft['customer'], 'items': items, 'freight': quoted_freight,
'destination': draft.get('destination'),
'total_cents': sum(i['total_cents'] for i in items)+quoted_freight['total_cents']}
c.execute('UPDATE dtf_local.quotes SET approved=%s, reviewed_by=%s, approved_at=now() WHERE id=%s', (Jsonb(approved),user,uid))
return approved
return quote_review.approve(c, row, body.items, user)
@router.post('/api/operator/orders/{uid}/move')
def move(uid: UUID, body: Move, user=Depends(operator)):

View File

@@ -1,4 +1,4 @@
"""Quotes: the customer's cart, and the operator-reviewed version of it."""
"""Quotes: the customer's cart, approved at once when it can be (app/quote_review.py)."""
import hashlib
import json
from decimal import Decimal
@@ -10,6 +10,7 @@ from psycopg.types.json import Jsonb
from ..core import db
from ..core.auth import owner
from ..core.models import QuoteRequest
from .. import quote_review
from ..runtime import freight, quote_view, require_delivery_available, upload_row
from ..scanning import require_clean
@@ -37,10 +38,14 @@ def create_quote(body: QuoteRequest, session_id=Depends(owner)):
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', (session_id, body.request_key)).fetchone()
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')
return {'id': row['id'], 'status': 'pending_review'}
reason = quote_review.review_reason(draft)
if row['id'] == uid and reason is None:
quote_review.approve(c, row, body.items, quote_review.AUTO)
return {'id': row['id'], 'status': 'approved'}
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)):