regluit/payment/models.py

567 lines
21 KiB
Python
Raw Normal View History

2013-06-03 16:31:39 +00:00
"""
external library imports
"""
2018-02-07 22:42:37 +00:00
import datetime
2013-06-03 16:31:39 +00:00
import uuid
import logging
from decimal import Decimal
from jsonfield import JSONField
2018-02-07 22:42:37 +00:00
## django imports
from django.conf import settings
2013-06-03 16:31:39 +00:00
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Q
from django.contrib.sites.models import Site
2018-02-07 22:42:37 +00:00
from django.db.models.signals import post_save, post_delete
from django.utils.http import urlquote
2018-04-19 16:24:34 +00:00
from django.utils.timezone import now
2018-02-07 22:42:37 +00:00
## django module imports
from notification import models as notification
2018-02-07 22:42:37 +00:00
## regluit imports
from regluit.payment.parameters import (
PAYMENT_TYPE_NONE,
PAYMENT_TYPE_AUTHORIZATION,
PAYMENT_HOST_NONE,
PAYMENT_HOST_CREDIT,
EXECUTE_TYPE_NONE,
TRANSACTION_STATUS_NONE,
TRANSACTION_STATUS_ACTIVE,
TRANSACTION_STATUS_ERROR,
TRANSACTION_STATUS_FAILED,
)
from regluit.payment.signals import credit_balance_added, pledge_created
2018-04-19 16:24:34 +00:00
from regluit.utils.localdatetime import date_today
logger = logging.getLogger(__name__)
# in fitting stripe -- here are possible fields to fit in with Transaction
2018-02-07 22:42:37 +00:00
# c.id, c.amount, c.amount_refunded, c.currency, c.description,
# datetime.fromtimestamp(c.created, tz=utc), c.paid,
# c.fee, c.disputed, c.amount_refunded, c.failure_message,
# c.card.fingerprint, c.card.type, c.card.last4, c.card.exp_month, c.card.exp_year
2012-09-17 21:55:28 +00:00
# promising fields
2011-09-27 12:48:11 +00:00
class Transaction(models.Model):
2018-02-07 22:42:37 +00:00
# type e.g., PAYMENT_TYPE_INSTANT or PAYMENT_TYPE_AUTHORIZATION -- defined in parameters.py
2011-09-27 12:48:11 +00:00
type = models.IntegerField(default=PAYMENT_TYPE_NONE, null=False)
2018-02-07 22:42:37 +00:00
# host: the payment processor.
#Named after the payment module that hosts the payment processing functions
host = models.CharField(default=PAYMENT_HOST_NONE, max_length=32, null=False)
2018-02-07 22:42:37 +00:00
#execution: e.g. EXECUTE_TYPE_CHAINED_INSTANT, EXECUTE_TYPE_CHAINED_DELAYED, EXECUTE_TYPE_PARALLEL
execution = models.IntegerField(default=EXECUTE_TYPE_NONE, null=False)
2018-02-07 22:42:37 +00:00
# status: general status constants defined in parameters.py
status = models.CharField(max_length=32, default=TRANSACTION_STATUS_NONE, null=False)
2018-02-07 22:42:37 +00:00
# local_status: status code specific to the payment processor
local_status = models.CharField(max_length=32, default='NONE', null=True)
2018-02-07 22:42:37 +00:00
# amount & currency -- amount of money and its currency involved for transaction
amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
max_amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
2011-09-27 12:48:11 +00:00
currency = models.CharField(max_length=10, default='USD', null=True)
2018-02-07 22:42:37 +00:00
# a unique ID that can be passed to PayPal to track a transaction
secret = models.CharField(max_length=64, null=True)
2018-02-07 22:42:37 +00:00
# a paykey that PayPal generates to identify this transaction
pay_key = models.CharField(max_length=128, null=True)
2018-02-07 22:42:37 +00:00
# a preapproval key that Paypal generates to identify this transaction
preapproval_key = models.CharField(max_length=128, null=True)
2018-02-07 22:42:37 +00:00
# (RY is not sure what receipt is for; t4u has hijacked this to be an email address for
# user.is_anonymous to send a receipt to)
2011-09-27 12:48:11 +00:00
receipt = models.CharField(max_length=256, null=True)
2018-02-07 22:42:37 +00:00
# whether a Preapproval has been approved or not
approved = models.NullBooleanField(null=True)
2018-02-07 22:42:37 +00:00
# error message from a transaction
2011-09-29 07:50:07 +00:00
error = models.CharField(max_length=256, null=True)
2018-02-07 22:42:37 +00:00
# IPN.reason_code
reason = models.CharField(max_length=64, null=True)
2018-02-07 22:42:37 +00:00
# creation and last modified timestamps
date_created = models.DateTimeField(auto_now_add=True, db_index=True,)
2011-09-27 12:48:11 +00:00
date_modified = models.DateTimeField(auto_now=True)
2018-02-07 22:42:37 +00:00
# date_payment: when an attempt is made to make the primary payment
2011-09-27 12:48:11 +00:00
date_payment = models.DateTimeField(null=True)
2018-02-07 22:42:37 +00:00
# date_executed: when an attempt is made to send money to non-primary chained receivers
date_executed = models.DateTimeField(null=True)
2018-02-07 22:42:37 +00:00
# datetime for creation of preapproval and for its expiration
2011-09-27 12:48:11 +00:00
date_authorized = models.DateTimeField(null=True)
date_expired = models.DateTimeField(null=True)
2018-02-07 22:42:37 +00:00
# associated User, Campaign, and Premium for this Transaction
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
campaign = models.ForeignKey('core.Campaign', on_delete=models.CASCADE, null=True)
premium = models.ForeignKey('core.Premium', on_delete=models.CASCADE, null=True)
offer = models.ForeignKey('core.Offer', on_delete=models.CASCADE, null=True)
extra = JSONField(null=True, default={})
2018-02-07 22:42:37 +00:00
# whether the user wants to be not listed publicly
2016-04-09 17:26:42 +00:00
anonymous = models.BooleanField(default=False)
2017-12-14 21:24:26 +00:00
# whether the transaction represents a donation
donation = models.BooleanField(default=False)
@property
def tier(self):
if self.amount < 25:
return 0
if self.amount < 50:
return 1
if self.amount < 100:
return 2
2018-02-07 22:42:37 +00:00
return 3
2017-02-13 18:33:26 +00:00
@property
def deadline_or_now(self):
if self.campaign and self.campaign.deadline:
return self.campaign.deadline
2018-02-07 22:42:37 +00:00
return now()
@property
def needed_amount(self):
if self.user is None or self.user.is_anonymous:
2014-02-20 03:18:23 +00:00
return self.max_amount
if self.user.credit.available >= self.max_amount:
return 0
2018-02-07 22:42:37 +00:00
return self.max_amount - self.user.credit.available
@property
def credit_amount(self):
if self.user is None or self.user.is_anonymous:
2014-02-20 20:56:10 +00:00
return 0
if self.user.credit.available >= self.max_amount:
return self.max_amount
return self.user.credit.available
2018-02-07 22:42:37 +00:00
@property
def ack_link(self):
return 'https://unglue.it/supporter/%s' % urlquote(self.user.username) if not self.anonymous else ''
2018-02-07 22:42:37 +00:00
def save(self, *args, **kwargs):
if not self.secret:
self.secret = str(uuid.uuid1())
super(Transaction, self).save(*args, **kwargs) # Call the "real" save() method.
2018-02-07 22:42:37 +00:00
2020-02-12 16:36:49 +00:00
def __str__(self):
2018-02-07 22:42:37 +00:00
return u"-- Transaction:\n \tstatus: %s\n \t amount: %s\n \terror: %s\n" % (self.status, str(self.amount), self.error)
def create_receivers(self, receiver_list):
2018-02-07 22:42:37 +00:00
primary = True
for r in receiver_list:
2018-02-07 22:42:37 +00:00
receiver = Receiver.objects.create(
email=r['email'],
amount=r['amount'],
currency=self.currency,
status="None",
primary=primary,
transaction=self
)
primary = False
2018-02-07 22:42:37 +00:00
def get_payment_class(self):
'''
Returns the specific payment processor that implements this transaction
'''
if self.host == PAYMENT_HOST_NONE:
return None
2018-02-07 22:42:37 +00:00
mod = __import__("regluit.payment." + self.host, fromlist=[str(self.host)])
return mod.Processor()
def set_executed(self):
self.date_executed = now()
self.save()
2013-12-15 06:26:42 +00:00
2018-02-07 22:42:37 +00:00
def set_payment(self):
2013-12-15 06:26:42 +00:00
self.date_payment = now()
2018-02-07 22:42:37 +00:00
self.save()
def set_credit_approved(self, amount):
2018-02-07 22:42:37 +00:00
self.amount = amount
self.host = PAYMENT_HOST_CREDIT
self.type = PAYMENT_TYPE_AUTHORIZATION
2018-02-07 22:42:37 +00:00
self.status = TRANSACTION_STATUS_ACTIVE
self.approved = True
now_val = now()
self.date_authorized = now_val
2018-02-07 22:42:37 +00:00
self.date_expired = now_val + datetime.timedelta(days=settings.PREAPPROVAL_PERIOD)
self.save()
pledge_created.send(sender=self, transaction=self)
2018-02-07 22:42:37 +00:00
def set_pledge_extra(self, pledge_extra):
if pledge_extra:
self.anonymous = pledge_extra.anonymous
self.premium = pledge_extra.premium
self.offer = pledge_extra.offer
2018-02-07 22:42:37 +00:00
self.extra.update(pledge_extra.extra)
2012-10-13 02:25:48 +00:00
def get_pledge_extra(self):
class pe:
2018-02-07 22:42:37 +00:00
anonymous = self.anonymous
premium = self.premium
offer = self.offer
extra = self.extra
return pe
@classmethod
2017-12-14 21:24:26 +00:00
def create(cls, amount=0.00, host=PAYMENT_HOST_NONE, max_amount=0.00, currency='USD',
2018-02-07 22:42:37 +00:00
status=TRANSACTION_STATUS_NONE, campaign=None, user=None, pledge_extra=None,
donation=False):
if user and user.is_anonymous:
2014-02-20 03:18:23 +00:00
user = None
2017-12-14 21:24:26 +00:00
t = cls.objects.create(
2018-02-07 22:42:37 +00:00
amount=amount,
host=host,
max_amount=max_amount,
currency=currency,
status=status,
campaign=campaign,
user=user,
donation=donation,
2017-12-14 21:24:26 +00:00
)
if pledge_extra:
t.set_pledge_extra(pledge_extra)
2017-12-14 21:24:26 +00:00
return t
2018-02-07 22:42:37 +00:00
class PaymentResponse(models.Model):
# The API used
api = models.CharField(max_length=64, null=False)
2018-02-07 22:42:37 +00:00
# The correlation ID
correlation_id = models.CharField(max_length=512, null=True)
2018-02-07 22:42:37 +00:00
# the paypal timestamp
timestamp = models.CharField(max_length=128, null=True)
2018-02-07 22:42:37 +00:00
# extra info we want to store if an error occurs such as the response message
info = models.CharField(max_length=1024, null=True)
2018-02-07 22:42:37 +00:00
# local status specific to the api call
status = models.CharField(max_length=32, null=True)
2018-02-07 22:42:37 +00:00
transaction = models.ForeignKey(Transaction, on_delete=models.CASCADE, null=False)
2020-02-12 16:36:49 +00:00
def __str__(self):
2018-02-07 22:42:37 +00:00
return u"PaymentResponse -- api: {0} correlation_id: {1} transaction: {2}".format(
self.api,
self.correlation_id,
2020-02-12 16:36:49 +00:00
str(self.transaction)
2018-02-07 22:42:37 +00:00
)
2011-09-27 12:48:11 +00:00
class Receiver(models.Model):
2018-02-07 22:42:37 +00:00
2011-09-27 12:48:11 +00:00
email = models.CharField(max_length=64)
2018-02-07 22:42:37 +00:00
amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
2011-09-27 12:48:11 +00:00
currency = models.CharField(max_length=10)
2011-09-27 12:48:11 +00:00
status = models.CharField(max_length=64)
local_status = models.CharField(max_length=64, null=True)
reason = models.CharField(max_length=64)
2016-04-08 22:12:10 +00:00
primary = models.BooleanField(default=True)
txn_id = models.CharField(max_length=64)
transaction = models.ForeignKey(Transaction, on_delete=models.CASCADE,)
2018-02-07 22:42:37 +00:00
2020-02-12 16:36:49 +00:00
def __str__(self):
2018-02-07 22:42:37 +00:00
return u"Receiver -- email: {0} status: {1} transaction: {2}".format(
self.email,
self.status,
2020-02-12 16:36:49 +00:00
str(self.transaction)
2018-02-07 22:42:37 +00:00
)
2012-08-07 02:52:45 +00:00
class CreditLog(models.Model):
2018-02-07 21:44:29 +00:00
# a write only record of Unglue.it Credit Transactions
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
timestamp = models.DateTimeField(auto_now=True)
action = models.CharField(max_length=16)
# used to record the sent id when action = 'deposit'
2018-02-07 22:42:37 +00:00
sent = models.IntegerField(null=True)
2012-08-07 02:52:45 +00:00
class Credit(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='credit')
balance = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
pledged = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
2012-08-07 02:52:45 +00:00
last_activity = models.DateTimeField(auto_now=True)
2018-02-07 22:42:37 +00:00
@property
def available(self):
return self.balance - self.pledged
2018-02-07 22:42:37 +00:00
2018-02-07 21:46:44 +00:00
def add_to_balance(self, num_credits, notify=True):
2018-02-07 22:42:37 +00:00
if self.pledged - self.balance > num_credits: # negative to withdraw
2012-08-07 02:52:45 +00:00
return False
else:
self.balance = self.balance + num_credits
self.save()
try: # bad things can happen here if you don't return True
2018-02-07 22:42:37 +00:00
CreditLog(user=self.user, amount=num_credits, action="add_to_balance").save()
except:
logger.exception("failed to log add_to_balance of %s", num_credits)
2018-02-07 21:46:44 +00:00
if notify:
2018-02-07 22:42:37 +00:00
try:
2018-02-07 21:46:44 +00:00
credit_balance_added.send(sender=self, amount=num_credits)
except:
logger.exception("credit_balance_added notification failed of %s", num_credits)
2012-08-07 02:52:45 +00:00
return True
2018-02-07 22:42:37 +00:00
2012-08-07 02:52:45 +00:00
def add_to_pledged(self, num_credits):
2018-02-07 22:42:37 +00:00
num_credits = Decimal(num_credits)
if num_credits is Decimal('NaN'):
2012-08-07 02:52:45 +00:00
return False
2018-02-07 22:42:37 +00:00
if self.balance - self.pledged < num_credits:
2012-08-07 02:52:45 +00:00
return False
2018-02-07 22:42:37 +00:00
self.pledged = self.pledged + num_credits
self.save()
try: # bad things can happen here if you don't return True
CreditLog(user=self.user, amount=num_credits, action="add_to_pledged").save()
except:
logger.exception("failed to log add_to_pledged of %s", num_credits)
return True
2012-08-07 02:52:45 +00:00
def use_pledge(self, num_credits):
2018-02-07 22:42:37 +00:00
num_credits = Decimal(num_credits)
if num_credits is Decimal('NaN'):
2012-08-07 02:52:45 +00:00
return False
2018-02-07 22:42:37 +00:00
if self.pledged < num_credits:
2012-08-07 02:52:45 +00:00
return False
2018-02-07 21:46:44 +00:00
self.pledged = self.pledged - num_credits
self.balance = self.balance - num_credits
self.save()
try:
2018-02-07 22:42:37 +00:00
CreditLog(user=self.user, amount=-num_credits, action="use_pledge").save()
2018-02-07 21:46:44 +00:00
except:
logger.exception("failed to log use_pledge of %s", num_credits)
return True
2018-02-07 22:42:37 +00:00
2018-02-07 21:46:44 +00:00
def transfer_to(self, receiver, num_credits, notify=True):
2018-02-07 22:42:37 +00:00
num_credits = Decimal(num_credits)
if num_credits is Decimal('NaN') or not isinstance(receiver, User):
logger.info('fail: %s, %s' % (num_credits, receiver))
2012-08-07 02:52:45 +00:00
return False
if self.add_to_balance(-num_credits):
2018-02-07 21:46:44 +00:00
if receiver.credit.add_to_balance(num_credits, notify):
2012-08-07 02:52:45 +00:00
return True
2018-02-07 22:42:37 +00:00
# unwind transfer
self.add_to_balance(num_credits, notify)
2012-08-07 02:52:45 +00:00
return False
2018-02-07 22:42:37 +00:00
return False
class Sent(models.Model):
'''used by gift view to record gifts it has sent'''
user = models.CharField(max_length=32, null=True)
amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
timestamp = models.DateTimeField(auto_now=True)
2018-02-07 22:42:37 +00:00
2012-09-13 23:43:54 +00:00
class Account(models.Model):
"""holds references to accounts at third party payment gateways, especially for representing credit cards"""
2018-02-07 22:42:37 +00:00
# the following fields from stripe Customer might be relevant to Account
# -- we need to pick good selection
# c.id, c.description, c.email, datetime.fromtimestamp(c.created, tz=utc),
# c.account_balance, c.delinquent,
# c.active_card.fingerprint, c.active_card.type, c.active_card.last4,
# c.active_card.exp_month, c.active_card.exp_year,
# c.active_card.country
2018-02-07 22:42:37 +00:00
# ACTIVE, DEACTIVATED, EXPIRED, EXPIRING, or ERROR
2018-02-07 22:42:37 +00:00
STATUS_CHOICES = (
('ACTIVE', 'ACTIVE'),
('DEACTIVATED', 'DEACTIVATED'),
('EXPIRED', 'EXPIRED'),
('EXPIRING', 'EXPIRING'),
('ERROR', 'ERROR')
)
# host: the payment processor. Named after the payment module that
# hosts the payment processing functions
2012-09-13 23:43:54 +00:00
host = models.CharField(default=PAYMENT_HOST_NONE, max_length=32, null=False)
account_id = models.CharField(max_length=128, null=True)
2018-02-07 22:42:37 +00:00
2012-09-17 21:55:28 +00:00
# card related info
card_last4 = models.CharField(max_length=4, null=True)
2018-02-07 22:42:37 +00:00
2012-09-17 21:55:28 +00:00
# Visa, American Express, MasterCard, Discover, JCB, Diners Club, or Unknown
card_type = models.CharField(max_length=32, null=True)
card_exp_month = models.IntegerField(null=True)
card_exp_year = models.IntegerField(null=True)
card_fingerprint = models.CharField(max_length=32, null=True)
card_country = models.CharField(max_length=2, null=True)
2018-02-07 22:42:37 +00:00
2012-09-17 21:55:28 +00:00
# creation and last modified timestamps
date_created = models.DateTimeField(auto_now_add=True)
2012-09-17 21:55:28 +00:00
date_modified = models.DateTimeField(auto_now=True)
date_deactivated = models.DateTimeField(null=True)
2018-02-07 22:42:37 +00:00
# associated User if any
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
2018-02-07 22:42:37 +00:00
2013-02-27 18:08:36 +00:00
# status variable
status = models.CharField(max_length=11, choices=STATUS_CHOICES, null=False, default='ACTIVE')
2018-02-07 22:42:37 +00:00
def deactivate(self):
2018-02-07 22:42:37 +00:00
"""Don't allow more than one active Account of given host to
be associated with a given user"""
self.date_deactivated = now()
self.status = 'DEACTIVATED'
self.save()
2018-02-07 22:42:37 +00:00
2013-02-27 18:08:36 +00:00
def calculated_status(self):
"""returns ACTIVE, DEACTIVATED, EXPIRED, EXPIRING, or ERROR"""
2018-02-07 22:42:37 +00:00
# is it deactivated?
2018-02-07 22:42:37 +00:00
today = date_today()
2018-02-07 22:42:37 +00:00
if self.date_deactivated is not None:
return 'DEACTIVATED'
2018-02-07 22:42:37 +00:00
# is it expired?
2018-02-07 22:42:37 +00:00
elif self.card_exp_year < today.year or (
self.card_exp_year == today.year and self.card_exp_month < today.month
):
return 'EXPIRED'
2018-02-07 22:42:37 +00:00
# about to expire? do I want to distinguish from 'ACTIVE'?
2018-02-07 22:42:37 +00:00
elif self.card_exp_year == today.year and self.card_exp_month == today.month:
return 'EXPIRING'
# any transactions w/ errors after the account date?
# Transaction.objects.filter(host='stripelib', status='Error', approved=True).count()
2018-02-07 22:42:37 +00:00
elif Transaction.objects.filter(host='stripelib',
status='Error', approved=True, user=self.user
).filter(date_payment__gt=self.date_created):
return 'ERROR'
2018-02-07 22:42:37 +00:00
return 'ACTIVE'
def update_status(self, value=None, send_notice_on_change_only=True):
"""set Account.status = value unless value is None, in which case,
we set Account.status=self.calculated_status()
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
fire off associated notifications
2018-02-07 22:42:37 +00:00
By default, send notices only if the status is *changing*.
Set send_notice_on_change_only = False to
2018-02-07 22:42:37 +00:00
send notice based on new_status regardless of old status.
(Useful for initialization)
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
"""
old_status = self.status
2018-02-07 22:42:37 +00:00
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
if value is None:
new_status = self.calculated_status()
else:
new_status = value
2018-02-07 22:42:37 +00:00
if new_status == 'EXPIRED':
self.deactivate()
elif old_status != new_status:
self.status = new_status
self.save()
2018-02-07 22:42:37 +00:00
# don't notify null users (non-users can buy-to-unglue or thank-for-ungluing)
if self.user and (not send_notice_on_change_only or (old_status != new_status)):
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
2018-02-07 22:42:37 +00:00
logger.info("Account status change: %d %s %s", self.pk, old_status, new_status)
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
if new_status == 'EXPIRING':
2018-02-07 22:42:37 +00:00
logger.info("EXPIRING. send to instance.user: %s site: %s", self.user,
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
Site.objects.get_current())
2018-02-07 22:42:37 +00:00
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
# fire off an account_expiring notice -- might not want to do this immediately
2018-02-07 22:42:37 +00:00
2016-04-08 22:45:50 +00:00
notification.queue([self.user], "account_expiring", {
'user': self.user,
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
'site':Site.objects.get_current()
}, True)
elif new_status == 'EXPIRED':
2018-02-07 22:42:37 +00:00
logger.info("EXPIRING. send to instance.user: %s site: %s", self.user,
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
Site.objects.get_current())
2018-02-07 22:42:37 +00:00
2016-04-08 22:45:50 +00:00
notification.queue([self.user], "account_expired", {
'user': self.user,
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
'site':Site.objects.get_current()
}, True)
2018-02-07 22:42:37 +00:00
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
elif new_status == 'ERROR':
# TO DO: what to do?
[#40140123] Add Account.update_status() to replace handle_Account_status_change thus resolving a number of issues Eric identified: * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221450 "It also seems odd to be using signals to talk only within the same module. This seems to me to be a misuse of signals, and makes for code that is harder to read" -- I need to decide whether I agree w/ Eric. * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4366503 long exchange "making it readonly would be overkill (java-ish) but the current code is not clear. Your save signal handler has to figure out whether a state transition has occurred and then act accordingly, which seems like a lot of hoops to jump though. better to put all the transition work in one place. For example, shouldn't it be the deactivate method that sets the status to deactivated? For example, the methods on campaign do a reasonable job of handling all the status transitions." What I should: mimic structure of https://github.com/Gluejar/regluit/blob/4fc449dad5c739e7ffb1e80109d6d76893d5638d/core/models.py#L322 -> time to write up how Campaign works * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4214827 "could also mean the Account has been deleted. don't you really want instance.pk=None?" * [ ]https://github.com/Gluejar/regluit/pull/176/files#r4221509 "since, these actions are being done in batch mode, the emit notifications would work a lot better if they were emitted after the batch of notifications is created."
2013-07-23 21:16:42 +00:00
pass
elif new_status == 'DEACTIVATED':
# nothing needs to happen here
pass
def recharge_failed_transactions(self):
2018-02-07 22:42:37 +00:00
"""When a new Account is saved, check whether this is the new active account for a user.
If so, recharge any outstanding failed transactions
"""
2018-02-07 22:42:37 +00:00
transactions_to_recharge = self.user.transaction_set.filter(
(Q(status=TRANSACTION_STATUS_FAILED) | Q(status=TRANSACTION_STATUS_ERROR)) &
Q(campaign__status='SUCCESSFUL')
).all()
if transactions_to_recharge:
from regluit.payment.manager import PaymentManager
pm = PaymentManager()
for transaction in transactions_to_recharge:
# check whether we are still within the window to recharge
2017-02-13 18:33:26 +00:00
if (now() - transaction.deadline_or_now) < datetime.timedelta(settings.RECHARGE_WINDOW):
2018-02-07 22:42:37 +00:00
logger.info("Recharging transaction {0} w/ status {1}".format(
transaction.id,
transaction.status
))
pm.execute_transaction(transaction, [])
2018-02-07 22:42:37 +00:00
# handle any save, updates to a payment.Transaction
def handle_transaction_change(sender, instance, created, **kwargs):
campaign = instance.campaign
if campaign:
campaign.update_left()
return True
# handle any deletes of payment.Transaction
def handle_transaction_delete(sender, instance, **kwargs):
campaign = instance.campaign
if campaign:
campaign.update_left()
return True
2018-02-07 22:42:37 +00:00
post_save.connect(handle_transaction_change, sender=Transaction)
post_delete.connect(handle_transaction_delete, sender=Transaction)
2012-11-12 20:48:03 +00:00
# handle recharging failed transactions