Как стать автором
Обновить

Комментарии 18

Можете пояснить зачем вы каждый раз вычисляете баланс? Вообще говоря это очень плохая идея, так-как чем дальше тем дольше он у вас будет считаться.
Мы его и не вычисляем каждый раз. Он денормализован и вычисляется только при записи транзакции.
Поэтому было решено денормализовать БД, добавив поле “balance” в модель пользователя. Данное поле обновляется в методе “save” в модели “UserBalanceChange”, а для уверенности в актуальности данных в нем, мы каждую ночь его пересчитываем.
Правильнее, конечно же, хранить информацию о текущем балансе пользователя в кэше (например, в Redis) и инвалидировать при каждом изменении модели.

Я вот про это. Последнее не надо. Как и пересчитывать баланс. Если у вас есть опасность неверности баланса, то сделайте таблицу истории балансов

class BalanceChange(models.Model):
    user = models.ForeignKey('User', related_name='balance_changes')
    user_balance_change = models.ForeignKey('UserBalanceChange', related_name='balance_changes')
    balance = models.DecimalField(_('Amount'), default=0, max_digits=18, decimal_places=6) 
    tsfrom = models.DateTimeField(_('date'), default=timezone.now)
    tsto = models.DateTimeField(_('date'), default=timezone.now)

Как-то так. Да и в этом случае можно отказаться вообще от хранения баланса. А в качестве текущего баланса берется запись с tsto со значением null.
Хранить данных примерно столько же, с UserBalanceChange общий баланс можно точно так же в транзакции обновлять (без агрегаций, через UPDATE… SET balance = balance — 100.00). С UserBalanceChange проще вставить событие «в середину», прошедшим числом, ну и поломать что-то сложнее.

Кроме того, вроде бы BalanceChange сложно заставить работать правильно, если несколько изменений баланса происходят одновременно — мы не можем правильно прочитать текущее значение баланса (его еще может не быть видно) => не можем правильно заполнить поле «balance» (а значит, если отказаться от хранения баланса, то он может стать неверным), и нам надо как-то разрешать конфликты по tsfrom / tsto.

UserBalanceChange — это, насколько понимаю, примерно реализация martinfowler.com/eaaDev/AccountingNarrative.html.

Я бы в нем еще поле reason сделал GFK вместо int, чтоб записи в UserBalanceChange связывать с событиями, которые к измемению баланса привели (например, к подписке или к реферралу).
except Exception, e: pass

Я надеюсь, что все-таки в оригинальном коде вместо «pass» присутствует корректный обработчик ошибок.
Да, конечно, там стоит обработчик и логгер.
А я надеюсь, что вместо «Exception, e» я начну все чаще видеть «Exception as e»
А я надеюсь что в оригинальном коде все таки не Exception, а ValueError
Арифметика с плавающей точкой в отношении денег в какой-то момент принесёт неожиданные сюрпризы.
habrahabr.ru/post/112953/

При очередной оплате домашнего интернета у меня на счету показывается что-то типа 449,9999890 вместо 450; и из-за этого однажды не списалась оплата с последующей приостановкой предоставления услуг.
Обратите внимание что тип amount вполне себе decimal
Да, спасибо, упустил
Для реализации этого механизма мы используем celery – написан task, который выполняется каждый час.

А почему celery, а не cron + commands?
Если это простенький сайтик с 2-3 задачами по расписанию, то тогда особого смыла конечно нет
Если проект достаточно сложный, то celery удобнее в плане управления и масштабирования.
При росте выносим все задачи на отдельный сервер(а) и удобно рулим ими.
Спасибо dlancer за ответ. Celery более гибко позволяет работать с задачами, причем напрямую из Django проекта. Отсутсвует необходимость «помнить» о расписании в cron'е.
В дополнение к вышесказанному:

Celery позволяет не только делать периодические задачи с заданными интервалами выполнения, но и отстреливать задачу с чётко заданным временем выполнения, и это время может быть вычислено динамически на основе пользовательских данных или чего либо другого.

Также, кроме просто выноса всех задач на отдельный сервер, можно задачи распределять на выполнение в разные очереди и назначать на каждую очередь своих обработчиков. Это позволяет не стопорить частые и быстрые задачи теми задачами, которые требуют больших ресурсов и работают долго.
А почему вы считате, что правильнее хранить баланс в кеше? Если вы используете транзакции, то никаких проблем с созданием транзакции и обновлением баланса быть не должно.
А зачем делать так, чтобы получить user?
order = OrderForPayment.objects.get(id=kwargs['InvId'])
user = User.objects.get(id=order.user.id)

Можное же просто
user = order.user


Зачем создавать и сразу же повторно сохранять?
balance_change_reason = UserBalanceChange.objects.create(
                user=user,
                reason=UserBalanceChange.TARIFF_HOUR_CHARGE,
                amount=-hour_rate,
    )
balance_change_reason.save()
Вообще странно открыть статью про биллинг и не увидеть ничего про транзакции на уровне БД.
Особенно изворот с балансом в этом плане сомнителен — должен обновляться в той же транзакции, которая добавляет UserBalanceChange.
Минус тут только один — чем больше записей в системе, тем больше время изменения баланса(пополнение\списание) и тут можно по всякому извращаться.

p.s. не в тот «уровень» ответил :(
Зарегистрируйтесь на Хабре , чтобы оставить комментарий