Files are uploaded before payment so the price and the security check use the file itself, but an abandoned cart kept them for 30 days. Now a finished upload is held 2 days, a quote waiting for review 7, an approved quote 2 more to be paid, and the paid order keeps its originals for 30 days from upload. A payment never starts for files that are gone; one under way holds them a day. Files attached to an order take the order's window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
4.8 KiB
Python
80 lines
4.8 KiB
Python
"""Attaching artwork to an order: the rules shared by customers and operators.
|
|
|
|
A customer submits a correction and an operator submits the final set; both go
|
|
through the same checks, so the rule about what may be attached, when, and what
|
|
it does to retention lives in one place.
|
|
"""
|
|
from datetime import datetime, timedelta, timezone
|
|
from uuid import UUID, uuid4
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from .runtime import upload_row
|
|
from .scanning import require_clean
|
|
|
|
|
|
def generated_owner(c, order, ref, kind):
|
|
"""The owner to look a generated print file up under, or None if it is not one.
|
|
|
|
A generated file may be approved as the final for the item it was made
|
|
from, and only while that item still has its original artwork: after a
|
|
customer correction it reproduces a layout nobody wants printed.
|
|
"""
|
|
generated = c.execute('''SELECT item_index FROM dtf_local.print_files
|
|
WHERE order_id=%s AND upload_id=%s AND status='ready' ''', (order['id'], ref.upload_id)).fetchone()
|
|
if not generated:
|
|
return None
|
|
if kind != 'final' or generated['item_index'] != ref.item_index:
|
|
raise HTTPException(422, 'A generated print file can only be the final file of its own item')
|
|
if c.execute("SELECT 1 FROM dtf_local.order_files WHERE order_id=%s AND kind='correction' LIMIT 1",
|
|
(order['id'],)).fetchone():
|
|
raise HTTPException(409, 'A customer correction replaced the artwork this file was generated from')
|
|
# A large sheet's print file is its original, owned by the customer.
|
|
return c.execute('SELECT owner FROM dtf_local.uploads WHERE id=%s', (ref.upload_id,)).fetchone()['owner']
|
|
|
|
def submit_files(c, order, body, identity, kind, actor):
|
|
if order['version'] != body.version:
|
|
raise HTTPException(409, 'Order changed. Refresh before submitting files.')
|
|
if kind == 'final' and order['state'] not in ('rec','tra','cor'):
|
|
raise HTTPException(409, 'Final files can only change during artwork review')
|
|
if kind == 'correction' and order['state'] != 'cor':
|
|
raise HTTPException(409, 'This order is not awaiting artwork correction')
|
|
if len({f.upload_id for f in body.files}) != len(body.files):
|
|
raise HTTPException(422, 'Each uploaded file must appear once')
|
|
count = len(order['snapshot']['items'])
|
|
if any(f.item_index >= count for f in body.files):
|
|
raise HTTPException(422, 'Invalid order item')
|
|
if kind == 'final' and {f.item_index for f in body.files} != set(range(count)):
|
|
raise HTTPException(422, 'Final-file set must cover every order item')
|
|
original_ids = [UUID(uid) for item in order['snapshot']['items'] for uid in item['uploads']]
|
|
first = c.execute('SELECT min(created_at) AS first FROM dtf_local.uploads WHERE id=ANY(%s)', (original_ids,)).fetchone()['first']
|
|
expiry = first + timedelta(days=30)
|
|
if expiry <= datetime.now(timezone.utc):
|
|
raise HTTPException(410, 'Order artwork retention has expired')
|
|
for ref in body.files:
|
|
upload = upload_row(c, ref.upload_id, generated_owner(c, order, ref, kind) or identity, lock=True)
|
|
if not upload['complete']:
|
|
raise HTTPException(409, 'Complete all uploads first')
|
|
require_clean(upload)
|
|
if c.execute('SELECT id FROM dtf_local.order_files WHERE upload_id=%s', (ref.upload_id,)).fetchone():
|
|
raise HTTPException(409, 'File is already attached. Upload a new revision.')
|
|
# A new customer correction supersedes every final prepared from earlier
|
|
# artwork, including finals uploaded while this order was in correction.
|
|
if kind == 'correction':
|
|
c.execute("UPDATE dtf_local.order_files SET active=false WHERE order_id=%s AND kind IN ('correction','final')", (order['id'],))
|
|
else:
|
|
c.execute('UPDATE dtf_local.order_files SET active=false WHERE order_id=%s AND kind=%s', (order['id'],kind))
|
|
for ref in body.files:
|
|
c.execute('INSERT INTO dtf_local.order_files(id,order_id,upload_id,item_index,kind,note,created_by) VALUES(%s,%s,%s,%s,%s,%s,%s)',
|
|
(uuid4(),order['id'],ref.upload_id,ref.item_index,kind,body.note,actor))
|
|
# The order's window, whatever the upload's own hold was.
|
|
c.execute('UPDATE dtf_local.uploads SET expires_at=%s WHERE id=%s', (expiry,ref.upload_id))
|
|
c.execute('UPDATE dtf_local.orders SET version=version+1,updated_at=now() WHERE id=%s', (order['id'],))
|
|
if kind == 'final':
|
|
# Artwork approval, not commercial quote approval, starts original cleanup.
|
|
# An original approved as its own print file is kept as the final.
|
|
finals = [ref.upload_id for ref in body.files]
|
|
c.execute("UPDATE dtf_local.uploads SET expires_at=LEAST(expires_at,now()+interval '7 days') WHERE id=ANY(%s) AND NOT id=ANY(%s)",
|
|
(original_ids, finals))
|
|
return {'ok': True, 'version': order['version']+1, 'expires_at': expiry}
|