chore: one-off data fix — paid-but-stuck offers + orphan cart_offers

Two ACCEPTED offers on shop.unturf.com were paid via PayPal but never
transitioned ACCEPTED → PAID because the cart-drain TypeError 502'd
the complete-checkout flow before mark_paid could fire (fixed by
ef91469 + 31e06ff + 6e44027). The /u/offers page still lists them
as Accepted with active Pay $X buttons that would re-charge.

Migration 73c5cb973915:
  1. Flips the two specific offer IDs to PAID, stamps
     paid_timestamp, writes a synthetic OFFER_EVENT_PAY entry in
     the offer-event log so the History panel reflects reality.
  2. Sweeps every cart_offer row whose offer is now in PAID state.
     These are orphans from pre-6e44027 drained carts — the
     symptom is an empty cart still rendering the green
     "Offer accepted" banner with a $X.XX total in the navbar
     even though the offer is settled.
  3. Same sweep for cart_auction → SETTLED auctions, for parity.

The flip-to-PAID is guarded — it only runs on offers still in
ACCEPTED state, so a re-run is a no-op. The cart_offer / cart_auction
sweep joins to terminal-state offers/auctions, so it's idempotent.
This commit is contained in:
russell@unturf.com 2026-05-15 09:20:36 -04:00
parent 5dbbe697b6
commit 1075004419
No known key found for this signature in database

View file

@ -0,0 +1,141 @@
"""one-off: mark stuck-ACCEPTED offers PAID + drop orphan cart_offers
Revision ID: 73c5cb973915
Revises: 882d68db47fa
Create Date: 2026-05-15 09:18:46.600361
One-shot data fix for shop.unturf.com:
Two offers were paid via PayPal but never transitioned ACCEPTED PAID
because of the cart.remove_product() TypeError that 502'd the
complete-checkout flow before mark_paid could fire:
ff6a2ae64fd711f1979d02dfe05770ee $6.00 countered to $6 then accepted
dd3ef09f4e5e11f1adea02dfe05770ee $21.00 buyer accepted seller's open
Mark them PAID, stamp paid_timestamp = now_ms, write a synthetic
OFFER_EVENT_PAY (event_type=6, actor=None=system, message="one-off
data fix paid via PayPal before mark_paid wiring landed").
Then sweep every cart_offer / cart_auction row whose linked offer or
auction is in a terminal-PAID state and the row is still alive. These
are orphans from carts that got drained but kept the association row
alive (pre-6e44027). Symptom: empty cart still rendering the green
"Offer accepted" banner with the agreed-price total in the navbar.
Safe to re-run: the offer-flip is no-op if state != ACCEPTED, the
cart_offer sweep is by-join-to-PAID so already-deleted rows simply
don't appear.
"""
from alembic import op
import sqlalchemy as sa
import uuid as _uuid
revision = "73c5cb973915"
down_revision = "882d68db47fa"
branch_labels = None
depends_on = None
# OFFER_STATE enum values, from make_post_sell/models/offer.py
OFFER_STATE_ACCEPTED = 1
OFFER_STATE_PAID = 6
OFFER_EVENT_PAY = 6
# Auction state value from make_post_sell/models/auction.py
AUCTION_STATE_SETTLED = 4
# The two offers fox flagged on shop.unturf.com /u/offers.
STUCK_OFFER_IDS = [
"ff6a2ae64fd711f1979d02dfe05770ee",
"dd3ef09f4e5e11f1adea02dfe05770ee",
]
def _ms_now():
import time
return int(time.time() * 1000)
def upgrade():
conn = op.get_bind()
now_ms = _ms_now()
# 1. Flip the two stuck offers to PAID + stamp paid_timestamp.
for offer_id in STUCK_OFFER_IDS:
result = conn.execute(
sa.text(
"SELECT state, current_amount_in_cents "
"FROM mps_offer WHERE id = :id"
),
{"id": offer_id},
).fetchone()
if result is None:
continue
state, current_amount = result
if state != OFFER_STATE_ACCEPTED:
# Already moved past ACCEPTED — leave alone.
continue
conn.execute(
sa.text(
"UPDATE mps_offer "
"SET state = :paid, "
" paid_timestamp = :ts, "
" last_action_timestamp = :ts "
"WHERE id = :id"
),
{"paid": OFFER_STATE_PAID, "ts": now_ms, "id": offer_id},
)
# Synthetic PAY event so the History panel reflects reality.
conn.execute(
sa.text(
"INSERT INTO mps_offer_event "
"(id, offer_id, event_type, actor_user_id, "
" amount_in_cents, message, created_timestamp) "
"VALUES (:id, :offer_id, :event_type, NULL, "
" :amount, :msg, :ts)"
),
{
"id": _uuid.uuid1().hex,
"offer_id": offer_id,
"event_type": OFFER_EVENT_PAY,
"amount": current_amount,
"msg": (
"one-off data fix — paid via PayPal before "
"mark_paid wiring landed (commits ef91469, 6e44027)"
),
"ts": now_ms,
},
)
# 2. Drop every cart_offer row whose offer is now in PAID state —
# these are orphans from carts that got drained pre-6e44027 but
# kept the association alive. They cause empty carts to still
# render the negotiated total in the navbar.
conn.execute(
sa.text(
"DELETE FROM mps_cart_offer "
"WHERE offer_id IN ("
" SELECT id FROM mps_offer WHERE state = :paid"
")"
),
{"paid": OFFER_STATE_PAID},
)
# 3. Same sweep for cart_auction → SETTLED auctions.
conn.execute(
sa.text(
"DELETE FROM mps_cart_auction "
"WHERE auction_id IN ("
" SELECT id FROM mps_auction WHERE state = :settled"
")"
),
{"settled": AUCTION_STATE_SETTLED},
)
def downgrade():
# Not reversible — we don't track which cart_offers we deleted.
pass