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>
82 lines
4.0 KiB
Python
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
|