pull/91/head
eric 2018-02-07 17:42:37 -05:00
parent 3084ab837c
commit cb50e931bd
3 changed files with 239 additions and 217 deletions

View File

@ -1,7 +1,4 @@
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.models import User
from datetime import datetime
from regluit.payment import baseprocessor
from regluit.payment.baseprocessor import BasePaymentRequest
@ -9,14 +6,12 @@ from regluit.payment.parameters import (
PAYMENT_HOST_CREDIT,
PAYMENT_TYPE_AUTHORIZATION,
PAYMENT_TYPE_INSTANT,
TRANSACTION_STATUS_ACTIVE,
TRANSACTION_STATUS_COMPLETE,
TRANSACTION_STATUS_ERROR,
TRANSACTION_STATUS_CANCELED,
)
from regluit.payment.signals import transaction_charged
def pledge_transaction(t,user,amount):
def pledge_transaction(t, user, amount):
"""commit <amount> from a <user>'s credit to a specified transaction <t>"""
if t.amount and t.host == PAYMENT_HOST_CREDIT:
@ -26,11 +21,11 @@ def pledge_transaction(t,user,amount):
success = user.credit.add_to_pledged(amount)
if success:
t.type = PAYMENT_TYPE_AUTHORIZATION
t.max_amount=amount
t.max_amount = amount
t.set_credit_approved(amount)
return success
def credit_transaction(t,user,amount):
def credit_transaction(t, user, amount):
'''user has new credit, use it to fund the transaction'''
# first, credit the user's account
success = user.credit.add_to_balance(amount)
@ -44,7 +39,7 @@ def credit_transaction(t,user,amount):
def pay_transaction(t, user, to_user, amount):
'''user has credit, transfer it to rh account'''
success = user.credit.transfer_to(to_user , amount)
success = user.credit.transfer_to(to_user, amount)
if success:
t.type = PAYMENT_TYPE_INSTANT
t.set_executed()
@ -60,10 +55,10 @@ class Processor(baseprocessor.Processor):
self.transaction = transaction
if transaction.user.credit.add_to_pledged(-transaction.amount):
#success
transaction.status=TRANSACTION_STATUS_CANCELED
transaction.status = TRANSACTION_STATUS_CANCELED
transaction.save()
else:
self.errorMessage="couldn't cancel the transaction"
self.errorMessage = "couldn't cancel the transaction"
self.status = 'Credit Cancel Failure'
class PreapprovalDetails(BasePaymentRequest):
@ -94,13 +89,8 @@ class Processor(baseprocessor.Processor):
user_to_pay = transaction.campaign.user_to_pay
credited = user_to_pay.credit.add_to_balance(amount, notify=False)
transaction.status = TRANSACTION_STATUS_COMPLETE
transaction.date_payment = now()
transaction.date_payment = datetime.now()
transaction.save()
# fire signal for sucessful transaction
transaction_charged.send(sender=self, transaction=transaction)

View File

@ -3,41 +3,52 @@ external library imports
"""
import datetime
import uuid
from django.utils.http import urlquote
import logging
from decimal import Decimal
from jsonfield import JSONField
"""
django imports
"""
## django imports
from django.conf import settings
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
from django.db.models.signals import post_save, post_delete, pre_save
from django.db.models.signals import post_save, post_delete
from django.utils.http import urlquote
## django module imports
"""
django module imports
"""
from notification import models as notification
"""
regluit imports
"""
from regluit.payment.parameters import *
## 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
from regluit.utils.localdatetime import now, date_today
logger = logging.getLogger(__name__)
# in fitting stripe -- here are possible fields to fit in with Transaction
# c.id, c.amount, c.amount_refunded, c.currency, c.description, datetime.fromtimestamp(c.created, tz=utc), c.paid,
# 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
@ -48,7 +59,8 @@ class Transaction(models.Model):
# type e.g., PAYMENT_TYPE_INSTANT or PAYMENT_TYPE_AUTHORIZATION -- defined in parameters.py
type = models.IntegerField(default=PAYMENT_TYPE_NONE, null=False)
# host: the payment processor. Named after the payment module that hosts the payment processing functions
# 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)
#execution: e.g. EXECUTE_TYPE_CHAINED_INSTANT, EXECUTE_TYPE_CHAINED_DELAYED, EXECUTE_TYPE_PARALLEL
@ -74,7 +86,8 @@ class Transaction(models.Model):
# a preapproval key that Paypal generates to identify this transaction
preapproval_key = models.CharField(max_length=128, null=True)
# (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)
# (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)
receipt = models.CharField(max_length=256, null=True)
# whether a Preapproval has been approved or not
@ -121,28 +134,25 @@ class Transaction(models.Model):
return 1
if self.amount < 100:
return 2
else:
return 3
@property
def deadline_or_now(self):
if self.campaign and self.campaign.deadline:
return self.campaign.deadline
else:
return now()
@property
def needed_amount(self):
if self.user == None or self.user.is_anonymous():
if self.user is None or self.user.is_anonymous():
return self.max_amount
if self.user.credit.available >= self.max_amount:
return 0
else:
return self.max_amount - self.user.credit.available
@property
def credit_amount(self):
if self.user == None or self.user.is_anonymous():
if self.user is None or self.user.is_anonymous():
return 0
if self.user.credit.available >= self.max_amount:
return self.max_amount
@ -164,7 +174,14 @@ class Transaction(models.Model):
primary = True
for r in receiver_list:
receiver = Receiver.objects.create(email=r['email'], amount=r['amount'], currency=self.currency, status="None", primary=primary, transaction=self)
receiver = Receiver.objects.create(
email=r['email'],
amount=r['amount'],
currency=self.currency,
status="None",
primary=primary,
transaction=self
)
primary = False
def get_payment_class(self):
@ -173,7 +190,6 @@ class Transaction(models.Model):
'''
if self.host == PAYMENT_HOST_NONE:
return None
else:
mod = __import__("regluit.payment." + self.host, fromlist=[str(self.host)])
return mod.Processor()
@ -186,14 +202,14 @@ class Transaction(models.Model):
self.save()
def set_credit_approved(self, amount):
self.amount=amount
self.amount = amount
self.host = PAYMENT_HOST_CREDIT
self.type = PAYMENT_TYPE_AUTHORIZATION
self.status=TRANSACTION_STATUS_ACTIVE
self.approved=True
self.status = TRANSACTION_STATUS_ACTIVE
self.approved = True
now_val = now()
self.date_authorized = now_val
self.date_expired = now_val + datetime.timedelta( days=settings.PREAPPROVAL_PERIOD )
self.date_expired = now_val + datetime.timedelta(days=settings.PREAPPROVAL_PERIOD)
self.save()
pledge_created.send(sender=self, transaction=self)
@ -202,14 +218,14 @@ class Transaction(models.Model):
self.anonymous = pledge_extra.anonymous
self.premium = pledge_extra.premium
self.offer = pledge_extra.offer
self.extra.update( pledge_extra.extra)
self.extra.update(pledge_extra.extra)
def get_pledge_extra(self):
class pe:
anonymous=self.anonymous
premium=self.premium
offer=self.offer
extra=self.extra
anonymous = self.anonymous
premium = self.premium
offer = self.offer
extra = self.extra
return pe
@classmethod
@ -251,7 +267,11 @@ class PaymentResponse(models.Model):
transaction = models.ForeignKey(Transaction, null=False)
def __unicode__(self):
return u"PaymentResponse -- api: {0} correlation_id: {1} transaction: {2}".format(self.api, self.correlation_id, unicode(self.transaction))
return u"PaymentResponse -- api: {0} correlation_id: {1} transaction: {2}".format(
self.api,
self.correlation_id,
unicode(self.transaction)
)
class Receiver(models.Model):
@ -269,7 +289,11 @@ class Receiver(models.Model):
transaction = models.ForeignKey(Transaction)
def __unicode__(self):
return u"Receiver -- email: {0} status: {1} transaction: {2}".format(self.email, self.status, unicode(self.transaction))
return u"Receiver -- email: {0} status: {1} transaction: {2}".format(
self.email,
self.status,
unicode(self.transaction)
)
class CreditLog(models.Model):
# a write only record of Unglue.it Credit Transactions
@ -278,7 +302,7 @@ class CreditLog(models.Model):
timestamp = models.DateTimeField(auto_now=True)
action = models.CharField(max_length=16)
# used to record the sent id when action = 'deposit'
sent=models.IntegerField(null=True)
sent = models.IntegerField(null=True)
class Credit(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='credit')
@ -291,13 +315,13 @@ class Credit(models.Model):
return self.balance - self.pledged
def add_to_balance(self, num_credits, notify=True):
if self.pledged - self.balance > num_credits : # negative to withdraw
if self.pledged - self.balance > num_credits: # negative to withdraw
return False
else:
self.balance = self.balance + 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_balance").save()
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)
if notify:
@ -308,48 +332,45 @@ class Credit(models.Model):
return True
def add_to_pledged(self, num_credits):
num_credits=Decimal(num_credits)
num_credits = Decimal(num_credits)
if num_credits is Decimal('NaN'):
return False
if self.balance - self.pledged < num_credits :
if self.balance - self.pledged < num_credits:
return False
else:
self.pledged=self.pledged + num_credits
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()
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
def use_pledge(self, num_credits):
num_credits=Decimal(num_credits)
num_credits = Decimal(num_credits)
if num_credits is Decimal('NaN'):
return False
if self.pledged < num_credits :
if self.pledged < num_credits:
return False
self.pledged = self.pledged - num_credits
self.balance = self.balance - num_credits
self.save()
try:
CreditLog(user = self.user, amount = - num_credits, action="use_pledge").save()
CreditLog(user=self.user, amount=-num_credits, action="use_pledge").save()
except:
logger.exception("failed to log use_pledge of %s", num_credits)
return True
def transfer_to(self, receiver, num_credits, notify=True):
num_credits=Decimal(num_credits)
if num_credits is Decimal('NaN') or not isinstance( receiver, User):
logger.info('fail: %s, %s' % (num_credits,receiver))
num_credits = Decimal(num_credits)
if num_credits is Decimal('NaN') or not isinstance(receiver, User):
logger.info('fail: %s, %s' % (num_credits, receiver))
return False
if self.add_to_balance(-num_credits):
if receiver.credit.add_to_balance(num_credits, notify):
return True
else:
# unwind transfer
self.add_to_balance(num_credits, notify)
return False
else:
return False
class Sent(models.Model):
@ -361,21 +382,25 @@ class Sent(models.Model):
class Account(models.Model):
"""holds references to accounts at third party payment gateways, especially for representing credit cards"""
# 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,
# 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
# ACTIVE, DEACTIVATED, EXPIRED, EXPIRING, or ERROR
STATUS_CHOICES = (
('ACTIVE','ACTIVE'),
('DEACTIVATED','DEACTIVATED'),
('EXPIRED','EXPIRED'),
('EXPIRING','EXPIRING'),
( 'ERROR','ERROR')
('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
# 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)
account_id = models.CharField(max_length=128, null=True)
@ -401,7 +426,8 @@ class Account(models.Model):
status = models.CharField(max_length=11, choices=STATUS_CHOICES, null=False, default='ACTIVE')
def deactivate(self):
"""Don't allow more than one active Account of given host to be associated with a given user"""
"""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()
@ -418,25 +444,27 @@ class Account(models.Model):
# is it expired?
elif self.card_exp_year < today.year or (self.card_exp_year == today.year and self.card_exp_month < today.month):
elif self.card_exp_year < today.year or (
self.card_exp_year == today.year and self.card_exp_month < today.month
):
return 'EXPIRED'
# about to expire? do I want to distinguish from 'ACTIVE'?
elif (self.card_exp_year == today.year and self.card_exp_month == today.month):
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()
elif Transaction.objects.filter(host='stripelib',
status='Error', approved=True, user=self.user).filter(date_payment__gt=self.date_created):
status='Error', approved=True, user=self.user
).filter(date_payment__gt=self.date_created):
return 'ERROR'
else:
return 'ACTIVE'
def update_status( self, value=None, send_notice_on_change_only=True):
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()
fire off associated notifications
@ -462,11 +490,11 @@ class Account(models.Model):
# 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)):
logger.info( "Account status change: %d %s %s", self.pk, old_status, new_status)
logger.info("Account status change: %d %s %s", self.pk, old_status, new_status)
if new_status == 'EXPIRING':
logger.info( "EXPIRING. send to instance.user: %s site: %s", self.user,
logger.info("EXPIRING. send to instance.user: %s site: %s", self.user,
Site.objects.get_current())
# fire off an account_expiring notice -- might not want to do this immediately
@ -477,7 +505,7 @@ class Account(models.Model):
}, True)
elif new_status == 'EXPIRED':
logger.info( "EXPIRING. send to instance.user: %s site: %s", self.user,
logger.info("EXPIRING. send to instance.user: %s site: %s", self.user,
Site.objects.get_current())
notification.queue([self.user], "account_expired", {
@ -494,10 +522,13 @@ class Account(models.Model):
pass
def recharge_failed_transactions(self):
"""When a new Account is saved, check whether this is the new active account for a user. If so, recharge any
outstanding failed transactions
"""When a new Account is saved, check whether this is the new active account for a user.
If so, recharge any outstanding failed transactions
"""
transactions_to_recharge = self.user.transaction_set.filter((Q(status=TRANSACTION_STATUS_FAILED) | Q(status=TRANSACTION_STATUS_ERROR)) & Q(campaign__status='SUCCESSFUL')).all()
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
@ -505,7 +536,10 @@ class Account(models.Model):
for transaction in transactions_to_recharge:
# check whether we are still within the window to recharge
if (now() - transaction.deadline_or_now) < datetime.timedelta(settings.RECHARGE_WINDOW):
logger.info("Recharging transaction {0} w/ status {1}".format(transaction.id, transaction.status))
logger.info("Recharging transaction {0} w/ status {1}".format(
transaction.id,
transaction.status
))
pm.execute_transaction(transaction, [])
@ -525,9 +559,7 @@ def handle_transaction_delete(sender, instance, **kwargs):
campaign.update_left()
return True
post_save.connect(handle_transaction_change,sender=Transaction)
post_delete.connect(handle_transaction_delete,sender=Transaction)
post_save.connect(handle_transaction_change, sender=Transaction)
post_delete.connect(handle_transaction_delete, sender=Transaction)
# handle recharging failed transactions