fix: harden week-two ordering, artwork and operations

This commit is contained in:
Cauê Faleiros
2026-09-23 10:40:18 -03:00
parent ccc25a2d5d
commit 24013458c9
43 changed files with 1064 additions and 228 deletions

View File

@@ -12,7 +12,7 @@ from ..artwork import submit_files
from ..core.auth import operator
from ..core.models import ArtworkSubmission, UploadStart
from ..runtime import file_rows, operator_identity
from .uploads import begin_upload, complete_upload, part_url, upload_status
from .uploads import begin_upload, cancel_upload, complete_upload, part_url, upload_status
router = APIRouter()
@@ -36,6 +36,10 @@ def final_part(uid: UUID, number: int, user=Depends(operator)):
def final_complete(uid: UUID, user=Depends(operator)):
return complete_upload(uid,session_id=operator_identity(user))
@router.delete('/api/operator/uploads/{uid}')
def final_cancel(uid: UUID, user=Depends(operator)):
return cancel_upload(uid,session_id=operator_identity(user))
@router.get('/api/operator/orders/{oid}/files')
def operator_files(oid: UUID, user=Depends(operator)):
with db.connect() as c:

View File

@@ -1,10 +1,9 @@
"""Health, session bootstrap and freight quoting."""
import os
from fastapi import APIRouter, HTTPException, Request, Response
from ..core import db
from ..core.auth import client_ip, owner, new_session, rate_limit
from ..core.limits import upload_limit_bytes
from ..core.models import Freight
from ..runtime import ENVIRONMENT, GUEST_SESSION_LIMIT, PART_BYTES, freight, storage
@@ -33,7 +32,7 @@ def session(request: Request, response: Response):
with db.connect() as c:
session_id = new_session(c, response)
return {'environment': ENVIRONMENT, 'cart_scope': str(session_id), 'part_bytes': PART_BYTES,
'max_upload_bytes': int(os.environ.get('MAX_UPLOAD_BYTES', '5368709120'))}
'max_upload_bytes': upload_limit_bytes()}
@router.post('/api/freight')
def quote_freight(body: Freight):

View File

@@ -3,9 +3,10 @@ import hashlib
import os
import secrets
from datetime import datetime, timedelta, timezone
from typing import Literal
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from psycopg.types.json import Jsonb
from ..core import db
@@ -65,15 +66,44 @@ def board(user=Depends(operator)):
finished = c.execute("SELECT * FROM dtf_local.orders WHERE state='fin' ORDER BY created_at DESC LIMIT %s",
(BOARD_FINISHED_LIMIT,)).fetchall()
finished_total = c.execute("SELECT count(*) AS n FROM dtf_local.orders WHERE state='fin'").fetchone()['n']
quotes = c.execute('''SELECT q.* FROM dtf_local.quotes q
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id WHERE o.id IS NULL
ORDER BY q.created_at LIMIT %s''', (BOARD_QUOTE_LIMIT,)).fetchall()
pending = c.execute('''SELECT q.* FROM dtf_local.quotes q
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id
WHERE o.id IS NULL AND q.approved IS NULL
ORDER BY q.created_at DESC,q.id DESC LIMIT %s''', (BOARD_QUOTE_LIMIT,)).fetchall()
approved = c.execute('''SELECT q.* FROM dtf_local.quotes q
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id
WHERE o.id IS NULL AND q.approved IS NOT NULL
ORDER BY q.created_at DESC,q.id DESC LIMIT 20''').fetchall()
pending_total = c.execute('''SELECT count(*) AS n FROM dtf_local.quotes q
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id
WHERE o.id IS NULL AND q.approved IS NULL''').fetchone()['n']
approved_total = c.execute('''SELECT count(*) AS n FROM dtf_local.quotes q
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id
WHERE o.id IS NULL AND q.approved IS NOT NULL''').fetchone()['n']
return {'states': STATES, 'transitions': TRANSITIONS,
'orders': active + list(reversed(finished)),
'finished_shown': len(finished), 'finished_total': finished_total,
'quotes': [quote_view(c, q) for q in quotes],
'quotes': [quote_view(c, q) for q in pending + approved],
'pending_total': pending_total, 'approved_total': approved_total,
'events': c.execute('SELECT * FROM dtf_local.outbox ORDER BY id DESC LIMIT 100').fetchall()}
@router.get('/api/operator/quotes')
def quote_page(kind: Literal['pending','approved'], before_created_at: datetime | None = None,
before_id: UUID | None = None, limit: int = Query(default=50, ge=1, le=100),
user=Depends(operator)):
if (before_created_at is None) != (before_id is None):
raise HTTPException(422, 'Both quote cursor fields are required')
approved_filter = 'q.approved IS NULL' if kind == 'pending' else 'q.approved IS NOT NULL'
cursor = 'AND (q.created_at,q.id)<(%s,%s)' if before_created_at else ''
params = ((before_created_at,before_id) if before_created_at else ()) + (limit+1,)
with db.connect() as c:
rows = c.execute(f'''SELECT q.* FROM dtf_local.quotes q
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id
WHERE o.id IS NULL AND {approved_filter} {cursor}
ORDER BY q.created_at DESC,q.id DESC LIMIT %s''', params).fetchall()
return {'quotes':[quote_view(c,row) for row in rows[:limit]],
'has_more':len(rows)>limit}
@router.post('/api/operator/quotes/{uid}/approve')
def approve(uid: UUID, body: Review, user=Depends(operator)):
with db.connect() as c:
@@ -83,15 +113,22 @@ def approve(uid: UUID, body: Review, user=Depends(operator)):
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']})
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,
'total_cents': sum(i['total_cents'] for i in items)+quoted_freight['total_cents']}

View File

@@ -26,8 +26,8 @@ def dev_paid(body: Pay, session_id=Depends(owner)):
quote = payments.approved_quote(c, body.quote_id, session_id)
except payments.PaymentRefused as refusal:
# The quote may already be paid; that is not a refusal.
existing = c.execute('SELECT * FROM dtf_local.orders WHERE quote_id=%s',
(body.quote_id,)).fetchone()
existing = c.execute('SELECT * FROM dtf_local.orders WHERE quote_id=%s AND owner=%s',
(body.quote_id, session_id)).fetchone()
if existing:
return existing
raise HTTPException(404 if 'not found' in str(refusal) else 409, str(refusal))

View File

@@ -1,6 +1,7 @@
"""Quotes: the customer's cart, and the operator-reviewed version of it."""
import hashlib
import json
from decimal import Decimal
from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException
@@ -16,6 +17,9 @@ 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')
draft = body.model_dump(mode='json', exclude={'request_key'})
digest = hashlib.sha256(json.dumps(draft, sort_keys=True).encode()).hexdigest()
try:

View File

@@ -12,6 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException
from ..core import db
from ..core.auth import audit, owner, rate_limit
from ..core.limits import upload_limit_bytes
from ..core.models import UploadStart
from ..runtime import PART_BYTES, storage, upload_row
@@ -19,8 +20,8 @@ router = APIRouter()
@router.post('/api/uploads')
def begin_upload(body: UploadStart, session_id=Depends(owner)):
if body.size > int(os.environ.get('MAX_UPLOAD_BYTES', '5368709120')):
raise HTTPException(413, 'File exceeds the upload limit')
if body.size > upload_limit_bytes():
raise HTTPException(413, 'File exceeds the malware scan limit; select a smaller file')
uid = uuid4()
key = f'originals/{uid}'
rate_limit('upload-start', str(session_id), 60, 900)
@@ -30,14 +31,16 @@ def begin_upload(body: UploadStart, session_id=Depends(owner)):
usage = c.execute('''SELECT COALESCE(sum(size),0) AS total,
COALESCE(sum(size) FILTER(WHERE owner=%s),0) AS owned,
count(*) FILTER(WHERE owner=%s AND NOT complete) AS pending
FROM dtf_local.uploads WHERE purged_at IS NULL''', (session_id,session_id)).fetchone()
FROM dtf_local.uploads WHERE purged_at IS NULL''',
(session_id,session_id)).fetchone()
if (usage['total']+body.size > int(os.environ.get('STORAGE_QUOTA_BYTES','53687091200')) or
usage['owned']+body.size > int(os.environ.get('OWNER_UPLOAD_QUOTA_BYTES','10737418240')) or
usage['pending'] >= int(os.environ.get('MAX_PENDING_UPLOADS','10'))):
audit('upload_quota_rejected')
raise HTTPException(429, 'Local storage quota or pending upload limit reached')
multipart = storage.begin(key)
c.execute('INSERT INTO dtf_local.uploads(id,owner,name,size,object_key,multipart_id) VALUES(%s,%s,%s,%s,%s,%s)',
c.execute('''INSERT INTO dtf_local.uploads(id,owner,name,size,object_key,multipart_id,expires_at)
VALUES(%s,%s,%s,%s,%s,%s,now()+interval '1 hour')''',
(uid, session_id, body.name, body.size, key, multipart))
return {'id': uid, 'part_bytes': PART_BYTES}
@@ -81,5 +84,16 @@ def complete_upload(uid: UUID, session_id=Depends(owner)):
existing_size = storage.size(row['object_key'])
if existing_size != row['size']:
raise HTTPException(409, 'Stored size differs from declared size')
c.execute('UPDATE dtf_local.uploads SET complete=true WHERE id=%s', (uid,))
c.execute("UPDATE dtf_local.uploads SET complete=true,expires_at=now()+interval '30 days' WHERE id=%s", (uid,))
return {'id': uid, 'complete': True}
@router.delete('/api/uploads/{uid}')
def cancel_upload(uid: UUID, session_id=Depends(owner)):
with db.connect() as c:
row=upload_row(c,uid,session_id,lock=True)
if row['complete']:
raise HTTPException(409, 'Completed upload cannot be cancelled')
storage.discard(row['object_key'],row['multipart_id'],False)
c.execute('UPDATE dtf_local.uploads SET purged_at=now() WHERE id=%s',(uid,))
audit('upload_cancelled', upload=str(uid))
return {'id':uid,'cancelled':True}