chore(billing): add backfill script for amount_usd on old purchase transactions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
783a89e825
commit
bbbca5bdf8
1 changed files with 90 additions and 0 deletions
90
backend/scripts/backfill_amount_usd.py
Normal file
90
backend/scripts/backfill_amount_usd.py
Normal file
|
|
@ -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_id> 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())
|
||||
Loading…
Add table
Reference in a new issue