Add needs_price_update field to Purchase model
Django CI/CD / test (push) Successful in 22s
Django CI/CD / build-and-push (push) Has been skipped

Replace fragile price change detection in Purchase.save() with a
lazy dirty flag approach. A pre_save/post_save signal pair detects
price/currency changes without extra DB queries, and convert_prices()
uses the flag to determine which purchases need conversion.

- Add needs_price_update BooleanField with db_index
- Add pre_save signal to store old price/currency values
- Add post_save signal to set needs_price_update=True when price/currency changes
- Simplify Purchase.save() to remove DB reload + comparison logic
- Remove price_or_currency_differ_from() method
- Update convert_prices() to filter on needs_price_update flag
- Extract _get_exchange_rate() and _save_converted_price() helpers
- Add tests for the new behavior
This commit is contained in:
2026-05-12 13:57:59 +02:00
parent a4e697a274
commit e3b53cd4a9
5 changed files with 209 additions and 66 deletions
+55 -45
View File
@@ -1,6 +1,7 @@
import logging
import requests
from django.db import models
from django.template.defaultfilters import floatformat
logger = logging.getLogger("games")
@@ -12,68 +13,77 @@ currency_to = "CZK"
currency_to = currency_to.upper()
def save_converted_info(purchase, converted_price, converted_currency):
def _get_exchange_rate(currency_from, currency_to, year):
logger.info(
f"Setting converted price of {purchase} to {converted_price} {converted_currency} (originally {purchase.price} {purchase.price_currency})"
f"[convert_prices]: Looking for exchange rate in database: {currency_from}->{currency_to}"
)
exchange_rate = ExchangeRate.objects.filter(
currency_from=currency_from, currency_to=currency_to, year=year
).first()
if not exchange_rate:
logger.info(
f"[convert_prices]: Getting exchange rate from {currency_from} to {currency_to} for {year}..."
)
try:
response = requests.get(
f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from.lower()}.json"
)
response.raise_for_status()
data = response.json()
currency_from_data = data.get(currency_from.lower())
rate = currency_from_data.get(currency_to.lower())
if rate:
logger.info(f"[convert_prices]: Got {rate}, saving...")
exchange_rate = ExchangeRate.objects.create(
currency_from=currency_from,
currency_to=currency_to,
year=year,
rate=floatformat(rate, 2),
)
exchange_rate = exchange_rate.rate
else:
logger.info("[convert_prices]: Could not get an exchange rate.")
except requests.RequestException as e:
logger.info(
f"[convert_prices]: Failed to fetch exchange rate for {currency_from}->{currency_to} in {year}: {e}"
)
elif exchange_rate:
exchange_rate = exchange_rate.rate
return exchange_rate
def _save_converted_price(purchase, converted_price, needs_update):
logger.info(
f"Setting converted price of {purchase} to {converted_price} {currency_to} (originally {purchase.price} {purchase.price_currency})"
)
purchase.converted_price = converted_price
purchase.converted_currency = converted_currency
purchase.save()
purchase.converted_currency = currency_to
if needs_update:
purchase.needs_price_update = False
purchase.save(update_fields=["converted_price", "converted_currency", "needs_price_update"])
def convert_prices():
purchases = Purchase.objects.filter(
converted_price__isnull=True, converted_currency=""
)
models.Q(needs_price_update=True) | models.Q(converted_price__isnull=True)
).distinct()
if purchases.count() == 0:
logger.info("[convert_prices]: No prices to convert.")
return
for purchase in purchases:
needs_update = purchase.needs_price_update
if purchase.price_currency.upper() == currency_to or purchase.price == 0:
save_converted_info(purchase, purchase.price, currency_to)
_save_converted_price(purchase, purchase.price, needs_update)
continue
year = purchase.date_purchased.year
currency_from = purchase.price_currency.upper()
exchange_rate = ExchangeRate.objects.filter(
currency_from=currency_from, currency_to=currency_to, year=year
).first()
logger.info(
f"[convert_prices]: Looking for exchange rate in database: {currency_from}->{currency_to}"
)
if not exchange_rate:
logger.info(
f"[convert_prices]: Getting exchange rate from {currency_from} to {currency_to} for {year}..."
)
try:
# this API endpoint only accepts lowercase currency string
response = requests.get(
f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from.lower()}.json"
)
response.raise_for_status()
data = response.json()
currency_from_data = data.get(currency_from.lower())
rate = currency_from_data.get(currency_to.lower())
if rate:
logger.info(f"[convert_prices]: Got {rate}, saving...")
exchange_rate = ExchangeRate.objects.create(
currency_from=currency_from,
currency_to=currency_to,
year=year,
rate=floatformat(rate, 2),
)
else:
logger.info("[convert_prices]: Could not get an exchange rate.")
except requests.RequestException as e:
logger.info(
f"[convert_prices]: Failed to fetch exchange rate for {currency_from}->{currency_to} in {year}: {e}"
)
exchange_rate = _get_exchange_rate(currency_from, currency_to, year)
if exchange_rate:
save_converted_info(
_save_converted_price(
purchase,
floatformat(purchase.price * exchange_rate.rate, 0),
currency_to,
floatformat(purchase.price * exchange_rate, 0),
needs_update,
)