diff --git a/backend/scripts/backfill_amount_usd.py b/backend/scripts/backfill_amount_usd.py new file mode 100644 index 00000000..734b7461 --- /dev/null +++ b/backend/scripts/backfill_amount_usd.py @@ -0,0 +1,90 @@ +""" +One-time migration: backfill amount_usd on purchase credit_transactions. + +Matches pack by: + 1. pack_id extracted from description "Purchased pack (...)" + 2. Fallback: closest credits count match from app_settings packs + 3. Fallback: test pack = $1.00 +""" +import asyncio +import re +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +PACK_PRICES = { + "starter": 49.0, + "pro": 199.0, + "scale": 499.0, + "test": 1.0, +} + +CREDITS_TO_PACK = { + 50: ("starter", 49.0), + 220: ("pro", 199.0), + 600: ("scale", 499.0), +} + + +def _resolve_price(description: str, credits: int) -> float | None: + """Return USD price for a transaction, or None if can't determine.""" + # Try to extract pack_id from description + m = re.match(r"Purchased\s+(\S+)\s+pack", description or "", re.IGNORECASE) + if m: + pack_id = m.group(1).lower() + if pack_id in PACK_PRICES: + return PACK_PRICES[pack_id] + + # Fallback: exact credits match + if credits in CREDITS_TO_PACK: + return CREDITS_TO_PACK[credits][1] + + # Fallback: closest credits match within 10% + for c, (_, price) in CREDITS_TO_PACK.items(): + if abs(c - credits) / max(c, 1) < 0.1: + return price + + return None + + +async def main(): + from motor.motor_asyncio import AsyncIOMotorClient + from app.config import Config + + mongo_uri = os.environ.get("MONGO_URI", Config.MONGO_URI) + db_name = mongo_uri.rstrip("/").split("/")[-1].split("?")[0] or "cohorta_db" + client = AsyncIOMotorClient(mongo_uri) + db = client[db_name] + + cursor = db.credit_transactions.find({ + "type": "purchase", + "amount_usd": {"$exists": False}, + }) + + updated = 0 + skipped = 0 + + async for tx in cursor: + description = tx.get("description", "") + credits = tx.get("amount", 0) + price = _resolve_price(description, credits) + + if price is None: + print(f" SKIP id={tx['_id']} desc='{description}' credits={credits} — cannot determine price") + skipped += 1 + continue + + await db.credit_transactions.update_one( + {"_id": tx["_id"]}, + {"$set": {"amount_usd": price}}, + ) + print(f" SET id={tx['_id']} desc='{description}' credits={credits} → ${price}") + updated += 1 + + print(f"\nDone: {updated} updated, {skipped} skipped.") + client.close() + + +if __name__ == "__main__": + asyncio.run(main())