feat: an unpaid cart's files are kept 2 days, a paid order's 30
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>
This commit is contained in:
@@ -85,6 +85,16 @@ def intent(body: PaymentIntent, session_id=Depends(owner)):
|
||||
raise HTTPException(404 if 'not found' in str(refusal) else 409, str(refusal))
|
||||
if c.execute('SELECT 1 FROM dtf_local.orders WHERE quote_id=%s', (body.quote_id,)).fetchone():
|
||||
raise HTTPException(409, 'Quote is already paid')
|
||||
# Never charge for files that are gone: an unpaid cart's files are
|
||||
# removed after a while. A payment under way keeps them a day longer,
|
||||
# time enough for the provider's notice to become the order.
|
||||
uploads = payments.quote_uploads(quote['approved'])
|
||||
live = c.execute('''SELECT count(*) AS n FROM dtf_local.uploads WHERE id=ANY(%s)
|
||||
AND purged_at IS NULL AND expires_at>now()''', (uploads,)).fetchone()['n']
|
||||
if live != len(set(uploads)):
|
||||
raise HTTPException(410, 'Os arquivos deste pedido expiraram porque ele não foi pago a tempo. '
|
||||
'Monte o pedido de novo para pagar.')
|
||||
payments.hold_uploads(c, uploads, '1 day')
|
||||
# Never a second charge: an approved payment is waiting for its
|
||||
# notification to become the order, and a card in review may still be.
|
||||
# A card waiting for the bank's confirmation (3-D Secure) blocks only
|
||||
|
||||
@@ -7,6 +7,7 @@ from uuid import UUID, uuid4
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from .. import payments
|
||||
from ..core import db
|
||||
from ..core.auth import owner
|
||||
from ..core.models import QuoteRequest
|
||||
@@ -50,6 +51,9 @@ def create_quote(body: QuoteRequest, session_id=Depends(owner)):
|
||||
if row['id'] == uid and reason is None:
|
||||
quote_review.approve(c, row, body.items, quote_review.AUTO)
|
||||
return {'id': row['id'], 'status': 'approved'}
|
||||
if row['id'] == uid:
|
||||
# An operator reviews it first; the files wait for that review.
|
||||
payments.hold_uploads(c, payments.quote_uploads(draft), payments.REVIEW_HOLD)
|
||||
return {'id': row['id'], 'status': 'approved' if row['approved'] else 'pending_review'}
|
||||
|
||||
@router.get('/api/quotes/{uid}')
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 ..payments import UNPAID_HOLD
|
||||
from ..runtime import PART_BYTES, storage, upload_row
|
||||
|
||||
router = APIRouter()
|
||||
@@ -87,7 +88,10 @@ 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,expires_at=now()+interval '30 days' WHERE id=%s", (uid,))
|
||||
# Held while the cart is unpaid: an abandoned cart's files go after
|
||||
# UNPAID_HOLD; a paid order keeps them for its 30 days (app/payments.py).
|
||||
c.execute('UPDATE dtf_local.uploads SET complete=true,expires_at=now()+%s::interval WHERE id=%s',
|
||||
(UNPAID_HOLD, uid))
|
||||
return {'id': uid, 'complete': True}
|
||||
|
||||
@router.delete('/api/uploads/{uid}')
|
||||
|
||||
Reference in New Issue
Block a user