regluit/payment/manager.py

922 lines
40 KiB
Python

from regluit.core.models import Campaign, Wishlist
from regluit.payment.models import Transaction, Receiver, PaymentResponse
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.conf import settings
from regluit.payment.parameters import *
from regluit.payment.paypal import IPN_SENDER_STATUS_COMPLETED
from regluit.payment.signals import transaction_charged, pledge_modified, pledge_created
if settings.PAYMENT_PROCESSOR == 'paypal':
from regluit.payment.paypal import Pay, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment
from regluit.payment.paypal import Pay as Execute
elif settings.PAYMENT_PROCESSOR == 'amazon':
from regluit.payment.amazon import Pay, Execute, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment
import uuid
import traceback
from regluit.utils.localdatetime import now
from dateutil.relativedelta import relativedelta
import logging
from decimal import Decimal as D
from xml.dom import minidom
import urllib, urlparse
from django.conf import settings
logger = logging.getLogger(__name__)
def append_element(doc, parent, name, text):
element = doc.createElement(name)
parent.appendChild(element)
text_node = doc.createTextNode(text)
element.appendChild(text_node)
return element
# at this point, there is no internal context and therefore, the methods of PaymentManager can be recast into static methods
class PaymentManager( object ):
def __init__( self, embedded=False):
self.embedded = embedded
def processIPN(self, request, module):
# Forward to our payment processor
mod = __import__("regluit.payment." + module, fromlist=[str(module)])
method = getattr(mod, "ProcessIPN")
return method(request)
def update_preapproval(self, transaction):
"""Update a transaction to hold the data from a PreapprovalDetails on that transaction"""
t = transaction
method = getattr(transaction.get_payment_class(), "PreapprovalDetails")
p = method(t)
preapproval_status = {'id':t.id, 'key':t.preapproval_key}
if p.error() or not p.success():
logger.info("Error retrieving preapproval details for transaction %d" % t.id)
preapproval_status["error"] = "An error occurred while verifying this transaction, see server logs for details"
else:
# Check the transaction status
if t.status != p.status:
preapproval_status["status"] = {'ours':t.status, 'theirs':p.status}
t.status = p.status
t.local_status = p.local_status
t.save()
# check the currency code
if t.currency != p.currency:
preapproval_status["currency"] = {'ours':t.currency, 'theirs':p.currency}
t.currency = p.currency
t.save()
# Check the amount
if t.max_amount != D(p.amount):
preapproval_status["amount"] = {'ours':t.max_amount, 'theirs':p.amount}
t.max_amount = p.amount
t.save()
# Check approved
if t.approved != p.approved:
preapproval_status["approved"] = {'ours':t.approved, 'theirs':p.approved}
t.approved = p.approved
t.save()
# In amazon FPS, we may not have a pay_key via the return URL, update here
try:
if t.pay_key != p.pay_key:
preapproval_status['pay_key'] = {'ours':t.pay_key, 'theirs':p.pay_key}
t.pay_key = p.pay_key
t.save()
except:
# No problem, p.pay_key is not defined for paypal function
blah = "blah"
return preapproval_status
def update_payment(self, transaction):
"""Update a transaction to hold the data from a PaymentDetails on that transaction"""
t = transaction
payment_status = {'id':t.id}
method = getattr(transaction.get_payment_class(), "PaymentDetails")
p = method(t)
if p.error() or not p.success():
logger.info("Error retrieving payment details for transaction %d" % t.id)
payment_status['error'] = "An error occurred while verifying this transaction, see server logs for details"
else:
# Check the transaction status
if t.status != p.status:
payment_status['status'] = {'ours': t.status, 'theirs': p.status}
t.status = p.status
t.local_status = p.local_status
t.save()
receivers_status = []
for r in p.transactions:
# This is only supported for paypal at this time
try:
receiver = Receiver.objects.get(transaction=t, email=r['email'])
receiver_status = {'email':r['email']}
logger.info(r)
logger.info(receiver)
# Check for updates on each receiver's status. Note that unprocessed delayed chained payments
# will not have a status code or txn id code
if receiver.status != r['status']:
receiver_status['status'] = {'ours': receiver.status, 'theirs': r['status']}
receiver.status = r['status']
receiver.save()
if receiver.txn_id != r['txn_id']:
receiver_status['txn_id'] = {'ours':receiver.txn_id, 'theirs':r['txn_id']}
receiver.txn_id = r['txn_id']
receiver.save()
except:
traceback.print_exc()
if not set(["status","txn_id"]).isdisjoint(receiver_status.keys()):
receivers_status.append(receiver_status)
if len(receivers_status):
payment_status["receivers"] = receivers_status
return payment_status
def checkStatus(self, past_days=None, transactions=None):
'''
Run through all pay transactions and verify that their current status is as we think.
Allow for a list of transactions to be passed in or for the method to check on all transactions within the
given past_days
'''
DEFAULT_DAYS_TO_CHECK = 3
status = {'payments':[], 'preapprovals':[]}
# look at all PAY transactions for stated number of past days; if past_days is not int, get all Transaction
# only PAY transactions have date_payment not None
if transactions is None:
if past_days is None:
past_days = DEFAULT_DAYS_TO_CHECK
try:
ref_date = now() - relativedelta(days=int(past_days))
payment_transactions = Transaction.objects.filter(date_payment__gte=ref_date)
except:
ref_date = now()
payment_transactions = Transaction.objects.filter(date_payment__isnull=False)
logger.info(payment_transactions)
# Now look for preapprovals that have not been paid and check on their status
preapproval_transactions = Transaction.objects.filter(date_authorized__gte=ref_date, date_payment=None, type=PAYMENT_TYPE_AUTHORIZATION)
logger.info(preapproval_transactions)
transactions = payment_transactions | preapproval_transactions
for t in transactions:
# deal with preapprovals
if t.date_payment is None:
preapproval_status = self.update_preapproval(t)
logger.info("transaction: {0}, preapproval_status: {1}".format(t, preapproval_status))
if not set(['status', 'currency', 'amount', 'approved']).isdisjoint(set(preapproval_status.keys())):
status["preapprovals"].append(preapproval_status)
# update payments
else:
payment_status = self.update_payment(t)
if not set(["status", "receivers"]).isdisjoint(payment_status.keys()):
status["payments"].append(payment_status)
# Clear out older, duplicate preapproval transactions
cleared_list = []
for p in transactions:
# pick out only the preapprovals
if p.date_payment is None and p.type == PAYMENT_TYPE_AUTHORIZATION and p.status == TRANSACTION_STATUS_ACTIVE and p not in cleared_list:
# keep only the newest transaction for this user and campaign
user_transactions_for_campaign = Transaction.objects.filter(user=p.user, status=TRANSACTION_STATUS_ACTIVE, campaign=p.campaign).order_by('-date_authorized')
if len(user_transactions_for_campaign) > 1:
logger.info("Found %d active transactions for campaign" % len(user_transactions_for_campaign))
self.cancel_related_transaction(user_transactions_for_campaign[0], status=TRANSACTION_STATUS_ACTIVE, campaign=transactions[0].campaign)
cleared_list.extend(user_transactions_for_campaign)
# Note, we may need to call checkstatus again here
return status
def run_query(self, transaction_list, summary=True, campaign_total=False, pledged=False, authorized=False, incomplete=False, completed=False, pending=False, error=False, failed=False, **kwargs):
'''
Generic query handler for returning summary and transaction info, see query_user and query_campaign
campaign_total=True includes all payment types which should count towards campaign total
'''
if campaign_total:
# must double check when adding Paypal or other
# return only ACTIVE transactions with approved=True
list = transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
approved=True).exclude(status=TRANSACTION_STATUS_CANCELED)
list = list | transaction_list.filter(type=PAYMENT_TYPE_INSTANT,
status=TRANSACTION_STATUS_COMPLETE)
else:
list = Transaction.objects.none()
if pledged:
list = list | transaction_list.filter(type=PAYMENT_TYPE_INSTANT,
status=TRANSACTION_STATUS_COMPLETE)
if authorized:
# return only ACTIVE transactions with approved=True
list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=TRANSACTION_STATUS_ACTIVE,
approved=True)
if incomplete:
list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=TRANSACTION_STATUS_INCOMPLETE)
if completed:
list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=TRANSACTION_STATUS_COMPLETE)
if pending:
list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=TRANSACTION_STATUS_PENDING)
if error:
list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=TRANSACTION_STATUS_ERROR)
if failed:
list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=TRANSACTION_STATUS_FAILED)
if summary:
amount = D('0.00')
for t in list:
if t.type==PAYMENT_TYPE_INSTANT:
for r in t.receiver_set.all():
#
# Currently receivers are only used for paypal, so keep the paypal status code here
#
if r.status == IPN_SENDER_STATUS_COMPLETED:
# or IPN_SENDER_STATUS_COMPLETED
# individual senders may not have been paid due to errors, and disputes/chargebacks only appear here
amount += r.amount
else:
amount += t.amount
return amount
else:
return list
def query_user(self, user, **kwargs):
'''
query_user
Returns either an amount or list of transactions for a user
summary: if true, return a float of the total, if false, return a list of transactions
return value: either a float summary or a list of transactions
Note: this method appears to be unused.
'''
transactions = Transaction.objects.filter(user=user)
return self.run_query(transactions, **kwargs)
def query_campaign(self, campaign, **kwargs ):
'''
query_campaign
Returns either an amount or list of transactions for a campaign
summary: if true, return a float of the total, if false, return a list of transactions
return value: either a float summary or a list of transactions
'''
transactions = Transaction.objects.filter(campaign=campaign)
return self.run_query(transactions, **kwargs)
def execute_campaign(self, campaign):
'''
execute_campaign
attempts to execute all pending transactions for a campaign.
return value: returns a list of transactions with the status of each receiver/transaction updated
'''
# only allow active transactions to go through again, if there is an error, intervention is needed
transactions = Transaction.objects.filter(campaign=campaign, status=TRANSACTION_STATUS_ACTIVE)
for t in transactions:
#
# Currently receivers are only used for paypal, so it is OK to leave the paypal info here
#
receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':t.amount},
{'email':campaign.paypal_receiver, 'amount':D(t.amount) * (D('1.00') - D(str(settings.GLUEJAR_COMMISSION)))}]
self.execute_transaction(t, receiver_list)
# TO DO: update campaign status
# Should this be done first before executing the transactions?
# How does the success/failure of transactions affect states of campaigns
return transactions
def finish_campaign(self, campaign):
'''
finish_campaign
attempts to execute all remaining payment to non-primary receivers
This is currently only supported for paypal
return value: returns a list of transactions with the status of each receiver/transaction updated
'''
# QUESTION: to figure out which transactions are in a state in which the payment to the primary recipient is done but not to secondary recipient
# Consider two possibilities: status=IPN_PAY_STATUS_INCOMPLETE or execution = EXECUTE_TYPE_CHAINED_DELAYED
# which one? Let's try the second one
# only allow incomplete transactions to go through again, if there is an error, intervention is needed
transactions = Transaction.objects.filter(campaign=campaign, execution=EXECUTE_TYPE_CHAINED_DELAYED)
for t in transactions:
result = self.finish_transaction(t)
# TO DO: update campaign status
return transactions
def cancel_campaign(self, campaign, reason="UNSUCCESSFUL CAMPAIGN"):
'''
cancel_campaign
attempts to cancel active preapprovals related to the campaign
return value: returns a list of transactions with the status of each receiver/transaction updated
'''
transactions = Transaction.objects.filter(campaign=campaign, status=TRANSACTION_STATUS_ACTIVE)
for t in transactions:
result = self.cancel_transaction(t)
if result:
t.reason = reason
t.save()
return transactions
def finish_transaction(self, transaction):
'''
finish_transaction
calls the paypal API to execute payment to non-primary receivers
transaction: the transaction we want to complete
'''
if transaction.execution != EXECUTE_TYPE_CHAINED_DELAYED:
logger.error("FinishTransaction called with invalid execution type")
return False
# mark this transaction as executed
transaction.date_executed = now()
transaction.save()
method = getattr(transaction.get_payment_class(), "Finish")
p = method(transaction)
# Create a response for this
envelope = p.envelope()
if envelope:
correlation = p.correlation_id()
timestamp = p.timestamp()
r = PaymentResponse.objects.create(api=p.url,
correlation_id = correlation,
timestamp = timestamp,
info = p.raw_response,
transaction=transaction)
if p.success() and not p.error():
logger.info("finish_transaction Success")
return True
else:
transaction.error = p.error_string()
transaction.save()
logger.info("finish_transaction error " + p.error_string())
return False
def execute_transaction(self, transaction, receiver_list):
'''
execute_transaction
executes a single pending transaction.
transaction: the transaction object to execute
receiver_list: a list of receivers for the transaction, in this format:
[
{'email':'email-1', 'amount':amount1},
{'email':'email-2', 'amount':amount2}
]
return value: a bool indicating the success or failure of the process. Please check the transaction status
after the IPN has completed for full information
'''
if len(transaction.receiver_set.all()) > 0:
# we are re-submitting a transaction, wipe the old receiver list
transaction.receiver_set.all().delete()
transaction.create_receivers(receiver_list)
# Mark as payment attempted so we will poll this periodically for status changes
transaction.date_payment = now()
transaction.save()
method = getattr(transaction.get_payment_class(), "Execute")
p = method(transaction)
# Create a response for this
envelope = p.envelope()
if envelope:
correlation = p.correlation_id()
timestamp = p.timestamp()
r = PaymentResponse.objects.create(api=p.api(),
correlation_id = correlation,
timestamp = timestamp,
info = p.raw_response,
transaction=transaction)
# We will update our transaction status when we receive the IPN
if p.success() and not p.error():
transaction.pay_key = p.key()
transaction.save()
logger.info("execute_transaction Success")
transaction_charged.send(sender=self, transaction=transaction)
return True
else:
transaction.error = p.error_string()
transaction.save()
logger.info("execute_transaction Error: " + p.error_string())
return False
def cancel_transaction(self, transaction):
'''
cancel
cancels a pre-approved transaction
return value: True if successful, false otherwise
'''
method = getattr(transaction.get_payment_class(), "CancelPreapproval")
p = method(transaction)
# Create a response for this
envelope = p.envelope()
if envelope:
correlation = p.correlation_id()
timestamp = p.timestamp()
r = PaymentResponse.objects.create(api=p.url,
correlation_id = correlation,
timestamp = timestamp,
info = p.raw_response,
transaction=transaction)
if p.success() and not p.error():
logger.info("Cancel Transaction " + str(transaction.id) + " Completed")
return True
else:
transaction.error = p.error_string()
transaction.save()
logger.info("Cancel Transaction " + str(transaction.id) + " Failed with error: " + p.error_string())
return False
def authorize(self, currency, target, amount, expiry=None, campaign=None, list=None, user=None,
return_url=None, nevermind_url=None, anonymous=False, premium=None,
paymentReason="unglue.it Pledge", ack_name=None, ack_link=None, ack_dedication=None,
modification=False):
'''
authorize
authorizes a set amount of money to be collected at a later date
currency: a 3-letter paypal currency code, i.e. USD
target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE
amount: the amount to authorize
campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN)
list: optional list object(to be set with TARGET_TYPE_LIST)
user: optional user object
return_url: url to redirect supporter to after a successful PayPal transaction
nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction
anonymous: whether this pledge is anonymous
premium: the premium selected by the supporter for this transaction
paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account
modification: whether this authorize call is part of a modification of an existing pledge
ack_name, ack_link, ack_dedication: how the user will be credited in the unglued ebook, if applicable
return value: a tuple of the new transaction object and a re-direct url. If the process fails,
the redirect url will be None
'''
t = Transaction.objects.create(amount=amount,
max_amount=amount,
type=PAYMENT_TYPE_AUTHORIZATION,
execution = EXECUTE_TYPE_CHAINED_DELAYED,
target=target,
currency=currency,
status='NONE',
campaign=campaign,
list=list,
user=user,
anonymous=anonymous,
premium=premium,
ack_name=ack_name,
ack_link=ack_link,
ack_dedication=ack_dedication
)
# we might want to not allow for a return_url or nevermind_url to be passed in but calculated
# here because we have immediate access to the Transaction object.
if nevermind_url is None:
nevermind_path = "{0}?{1}".format(reverse('pledge_nevermind'),
urllib.urlencode({'tid':t.id}))
nevermind_url = urlparse.urljoin(settings.BASE_URL, nevermind_path)
if return_url is None:
return_path = "{0}?{1}".format(reverse('pledge_complete'),
urllib.urlencode({'tid':t.id}))
return_url = urlparse.urljoin(settings.BASE_URL, return_path)
method = getattr(t.get_payment_class(), "Preapproval")
p = method(t, amount, expiry, return_url=return_url, nevermind_url=nevermind_url, paymentReason=paymentReason)
# Create a response for this
envelope = p.envelope()
if envelope:
r = PaymentResponse.objects.create(api=p.url,
correlation_id = p.correlation_id(),
timestamp = p.timestamp(),
info = p.raw_response,
transaction=t)
if p.success() and not p.error():
t.preapproval_key = p.key()
t.save()
url = p.next_url()
logger.info("Authorize Success: " + url)
# modification and initial pledge use different notification templates --
# decide which to send
# we need to fire notifications at the first point at which we are sure
# that the transaction has successfully completed; triggering notifications
# when the transaction is initiated risks sending notifications on transactions
# that for whatever reason fail. will need other housekeeping to handle those.
# sadly this point is not yet late enough in the process -- needs to be moved
# until after we are certain.
if not modification:
# BUGBUG:
# send the notice here for now
# this is actually premature since we're only about to send the user off to the payment system to
# authorize a charge
pledge_created.send(sender=self, transaction=t)
return t, url
else:
t.error = p.error_string()
t.save()
logger.info("Authorize Error: " + p.error_string())
return t, None
def cancel_related_transaction(self, transaction, status=TRANSACTION_STATUS_ACTIVE, campaign=None):
'''
Cancels any other similar status transactions for the same campaign. Used with modify code
Returns the number of transactions successfully canceled
'''
related_transactions = Transaction.objects.filter(status=status, user=transaction.user)
if len(related_transactions) == 0:
return 0
if campaign:
related_transactions = related_transactions.filter(campaign=campaign)
canceled = 0
for t in related_transactions:
if t.id == transaction.id:
# keep our transaction
continue
if self.cancel_transaction(t):
canceled = canceled + 1
# send notice about modification of transaction
if transaction.amount > t.amount:
# this should be the only one that happens
up_or_down = "increased"
elif transaction.amount < t.amount:
# we shouldn't expect any case in which this happens
up_or_down = "decreased"
else:
# we shouldn't expect any case in which this happens
up_or_down = None
pledge_modified.send(sender=self, transaction=transaction, up_or_down=up_or_down)
else:
logger.error("Failed to cancel transaction {0} for related transaction {1} ".format(t, transaction))
return canceled
def modify_transaction(self, transaction, amount, expiry=None, anonymous=None, premium=None,
return_url=None, nevermind_url=None, paymentReason=None,
ack_name=None, ack_link=None, ack_dedication=None):
'''
modify
Modifies a transaction. The only type of modification allowed is to the amount and expiration date
amount: the new amount
expiry: the new expiration date, or if none the current expiration date will be used
anonymous: new anonymous value; if None, then keep old value
premium: new premium selected; if None, then keep old value
return_url: the return URL after the preapproval(if needed)
nevermind_url: the cancel url after the preapproval(if needed)
paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account
return value: True if successful, False otherwise. An optional second parameter for the forward URL if a new authorhization is needed
'''
# Can only modify the amount of a preapproval for now
if transaction.type != PAYMENT_TYPE_AUTHORIZATION:
logger.info("Error, attempt to modify an invalid transaction type")
return False, None
# Can only modify an active, pending transaction. If it is completed, we need to do a refund. If it is incomplete,
# then an IPN may be pending and we cannot touch it
if transaction.status != TRANSACTION_STATUS_ACTIVE:
logger.info("Error, attempt to modify a transaction that is not active")
return False, None
# if any of expiry, anonymous, or premium is None, use the existing value
if expiry is None:
expiry = transaction.date_expired
if anonymous is None:
anonymous = transaction.anonymous
if premium is None:
premium = transaction.premium
if amount > transaction.max_amount or expiry != transaction.date_expired:
# Start a new authorization for the new amount
t, url = self.authorize(transaction.currency,
transaction.target,
amount,
expiry,
transaction.campaign,
transaction.list,
transaction.user,
return_url,
nevermind_url,
anonymous,
premium,
paymentReason,
ack_name,
ack_link,
ack_dedication,
True)
if t and url:
# Need to re-direct to approve the transaction
logger.info("New authorization needed, redirection to url %s" % url)
# Do not cancel the transaction here, wait until we get confirmation that the transaction is complete
# then cancel all other active transactions for this campaign
#self.cancel_transaction(transaction)
# while it would seem to make sense to send a pledge notification change here
# if we do, we will also send notifications when we initiate but do not
# successfully complete a pledge modification
return True, url
else:
# a problem in authorize
logger.info("Error, unable to start a new authorization")
# should we send a pledge_modified signal with state="failed" and a
# corresponding notification to the user? that would go here.
return False, None
elif amount <= transaction.max_amount:
# Update transaction but leave the preapproval alone
transaction.amount = amount
transaction.anonymous = anonymous
transaction.premium = premium
transaction.ack_name = ack_name
transaction.ack_link = ack_link
transaction.ack_dedication = ack_dedication
transaction.save()
logger.info("Updated amount of transaction to %f" % amount)
# since modifying pledges downwards happens immediately and only within our
# db, we don't have to wait until we hear back from amazon to be assured of
# success; send the notification immediately
pledge_modified.send(sender=self, transaction=transaction, up_or_down="decreased")
return True, None
else:
# this shouldn't happen
return False, None
def refund_transaction(self, transaction):
'''
refund
Refunds a transaction. The money for the transaction may have gone to a number of places. We can only
refund money that is in our account
return value: True if successful, false otherwise
'''
# First check if a payment has been made. It is possible that some of the receivers may be incomplete
# We need to verify that the refund API will cancel these
if transaction.status != TRANSACTION_STATUS_COMPLETE:
logger.info("Refund Transaction failed, invalid transaction status")
return False
method = getattr(transaction.get_payment_class(), "RefundPayment")
p = method(transaction)
# Create a response for this
envelope = p.envelope()
if envelope:
correlation = p.correlation_id()
timestamp = p.timestamp()
r = PaymentResponse.objects.create(api=p.url,
correlation_id = correlation,
timestamp = timestamp,
info = p.raw_response,
transaction=transaction)
if p.success() and not p.error():
logger.info("Refund Transaction " + str(transaction.id) + " Completed")
return True
else:
transaction.error = p.error_string()
transaction.save()
logger.info("Refund Transaction " + str(transaction.id) + " Failed with error: " + p.error_string())
return False
def pledge(self, currency, target, receiver_list, campaign=None, list=None, user=None,
return_url=None, nevermind_url=None, anonymous=False, premium=None, ack_name=None,
ack_link=None, ack_dedication=None):
'''
pledge
Performs an instant payment
currency: a 3-letter paypal currency code, i.e. USD
target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE
receiver_list: a list of receivers for the transaction, in this format:
[
{'email':'email-1', 'amount':amount1},
{'email':'email-2', 'amount':amount2}
]
campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN)
list: optional list object(to be set with TARGET_TYPE_LIST)
user: optional user object
return_url: url to redirect supporter to after a successful PayPal transaction
nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction
anonymous: whether this pledge is anonymous
premium: the premium selected by the supporter for this transaction
return value: a tuple of the new transaction object and a re-direct url. If the process fails,
the redirect url will be None
'''
amount = D('0.00')
# for chained payments, first amount is the total amount
amount = D(receiver_list[0]['amount'])
t = Transaction.objects.create(amount=amount,
max_amount=amount,
type=PAYMENT_TYPE_INSTANT,
execution=EXECUTE_TYPE_CHAINED_INSTANT,
target=target,
currency=currency,
status='NONE',
campaign=campaign,
list=list,
user=user,
date_payment=now(),
anonymous=anonymous,
premium=premium,
ack_name=ack_name,
ack_link=ack_link,
ack_dedication=ack_dedication
)
t.create_receivers(receiver_list)
method = getattr(t.get_payment_class(), "Pay")
p = method(t,return_url=return_url, nevermind_url=nevermind_url)
# Create a response for this
envelope = p.envelope()
logger.info(envelope)
if envelope:
r = PaymentResponse.objects.create(api=p.api(),
correlation_id = p.correlation_id(),
timestamp = p.timestamp(),
info = p.raw_response,
transaction=t)
if p.success() and not p.error():
t.pay_key = p.key()
t.status = TRANSACTION_STATUS_CREATED
t.save()
if self.embedded:
url = p.embedded_url()
logger.info(url)
else:
url = p.next_url()
logger.info("Pledge Success: " + url)
return t, url
else:
t.error = p.error_string()
t.save()
logger.info("Pledge Error: %s" % p.error_string())
return t, None