Files
dtf-system/app/quote_review.py
Cauê Faleiros 275ebf72c4
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
feat: approve priced carts at checkout so customers can pay at once
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>
2026-09-28 11:02:56 -03:00

82 lines
4.0 KiB
Python

"""Quote approval: automatic at checkout, by an operator for the exceptions.
A quote the customer can pay is priced here from the server's own ladders,
whether an operator approved it on the Kanban or the Site approved it the
moment it was created. The Site is a shop: a customer who can price the order
should be able to pay for it straight away, at any hour, so only orders a
person must look at before charging wait for the Kanban (review_reason).
Known limit: the grade (and so the discount) and the layout are still worked
out in the customer's browser (roadmap 3.2, 3.9). The API checks the layout's
geometry and that an unanalysed item carries no discount, but a customer who
edits the page can claim a better grade. Operators see every order's grade
and artwork at Arte recebida.
"""
import os
from decimal import Decimal
from fastapi import HTTPException
from psycopg.types.json import Jsonb
from .core.pricing import price
from .runtime import freight, upload_row
from .scanning import require_clean
AUTO = 'auto'
def auto_approve_enabled():
return os.environ.get('QUOTE_AUTO_APPROVE', 'true').lower() == 'true'
def max_auto_metres():
return Decimal(os.environ.get('QUOTE_AUTO_MAX_METRES', '50'))
def review_reason(draft):
"""Why this quote needs a person before it can be paid, or None."""
if not auto_approve_enabled():
return 'Aprovação automática desligada'
total = sum(Decimal(str(item['metres'])) for item in draft['items'])
if total > max_auto_metres():
return f'Pedido acima de {max_auto_metres():g} m'
for item in draft['items']:
if item.get('production', {}).get('version') != 2:
return 'Montagem antiga'
# The Site grades only the art it could analyse; anything else is
# priced at the full rate. A discount on it did not come from the Site.
if item['quality_status'] == 'unverified' and item['grade'] != 0:
return 'Nota informada sem análise da arte'
return None
def approve(c, row, items, reviewer):
"""Price the reviewed items and bind them to the quote; returns the approval.
`row` must be locked by the caller."""
draft = row['draft']
if row['approved']:
raise HTTPException(409, 'Approved quotes are immutable; request a new quote')
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(items) != len(draft['items']):
raise HTTPException(422, 'Review must cover every item')
priced = []
for item, original in zip(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']))
priced.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': priced, 'freight': quoted_freight,
'destination': draft.get('destination'),
'total_cents': sum(i['total_cents'] for i in priced) + quoted_freight['total_cents']}
c.execute('UPDATE dtf_local.quotes SET approved=%s, reviewed_by=%s, approved_at=now() WHERE id=%s',
(Jsonb(approved), reviewer, row['id']))
return approved