Adding payment amazon module - IPN, Refund, Cancel, PaymentDetails support

pull/1/head
icellama21 2012-04-21 01:11:16 -04:00
parent 05fa8d83a8
commit b1dbf8642f
1 changed files with 273 additions and 61 deletions

View File

@ -2,34 +2,18 @@ from regluit.payment.parameters import *
from django.core.urlresolvers import reverse
from django.conf import settings
from regluit.payment.models import Transaction, PaymentResponse
from django.contrib.auth.models import User
from django.utils import simplejson as json
from django.utils.xmlutils import SimplerXMLGenerator
from django.db import IntegrityError
from django.db.models.query_utils import Q
from django.shortcuts import render_to_response
from django.template import RequestContext
from boto.fps.connection import FPSConnection
from django.http import HttpResponse, HttpRequest, HttpResponseRedirect, HttpResponseBadRequest
from django.http import HttpResponse, HttpRequest, HttpResponseRedirect, HttpResponseBadRequest, HttpResponseForbidden
from datetime import timedelta
from regluit.utils.localdatetime import now, zuluformat
import dateutil
from boto import handler
from boto.resultset import ResultSet
import xml.sax
import dateutil.parser
import hashlib
import httplib
import traceback
import datetime
import uuid
import os
import urllib
import urllib2
import logging
import random
import commands
import smtplib
import urlparse
import decimal
import time
logger = logging.getLogger(__name__)
@ -44,35 +28,157 @@ AMAZON_STATUS_PAYMENT_MISMATCH = 'PE'
AMAZON_STATUS_INCOMPLETE = 'NP'
AMAZON_STATUS_NOT_REGISTERED = 'NM'
AMAZON_STATUS_CANCELLED = 'Cancelled'
AMAZON_STATUS_CANCELED = 'Canceled'
AMAZON_STATUS_FAILURE = 'Failure'
AMAZON_STATUS_PENDING = 'Pending'
AMAZON_STATUS_RESERVED = 'Reserved'
AMAZON_STATUS_SUCCESS = 'Success'
AMAZON_IPN_STATUS_CANCELLED = 'CANCELLED'
AMAZON_IPN_STATUS_CANCELED = 'CANCELED'
AMAZON_IPN_STATUS_FAILURE = 'FAILURE'
AMAZON_IPN_STATUS_PENDING = 'PENDING'
AMAZON_IPN_STATUS_RESERVED = 'RESERVED'
AMAZON_IPN_STATUS_SUCCESS = 'SUCCESS'
AMAZON_NOTIFICATION_TYPE_STATUS = 'TransactionStatus'
AMAZON_NOTIFICATION_TYPE_CANCEL = 'TokenCancellation'
AMAZON_OPERATION_TYPE_PAY = 'PAY'
AMAZON_OPERATION_TYPE_REFUND = 'REFUND'
AMAZON_OPERATION_TYPE_CANCEL = 'CANCEL'
def ProcessIPN(request):
'''
IPN handler for amazon
IPN handler for amazon. Here is a litle background on amazon IPNS
notificationType: Can either be TransactionStatus of TokenCancellation
status: One of the defined IPN status codes
operation: The type of operation
callerReference: The reference to find the transaction
The IPN is called for the following cases:
A payment or reserve succeeds
A payment or reserve fails
A payment or reserve goes into a pending state
A reserved payment is settled successfully
A reserved payment is not settled successfully
A refund succeeds
A refund fails
A refund goes into a pending state
A payment is canceled
A reserve is canceled
A token is canceled successfully
'''
try:
print "Process Amazon IPN"
uri = request.build_absolute_uri()
parsed_url = urlparse.urlparse(uri)
connection = FPSConnection(settings.FPS_ACCESS_KEY, settings.FPS_SECRET_KEY)
# Check the validity of the IPN
resp = connection.verify_signature("%s://%s%s" %(parsed_url.scheme,
parsed_url.netloc,
parsed_url.path),
request.raw_post_data)
if not resp[0].VerificationStatus == "Success":
# Error, ignore this IPN
logging.error("Amazon IPN cannot be verified with post data: ")
logging.error(request.raw_post_data)
return HttpResponseForbidden()
print "Amazon IPN POST DATA:"
print request.POST
reference = request.POST['callerReference']
type = request.POST['notificationType']
# In the case of cancelling a token, there is no transaction, so this info is not set
transactionId = request.POST.get('transactionId', None)
date = request.POST.get('transactionDate', None)
operation = request.POST.get('operation', None)
# We should always find the transaction by the token
transaction = Transaction.objects.get(secret=reference)
if type == AMAZON_NOTIFICATION_TYPE_STATUS:
status = request.POST['transactionStatus']
# status update for the token, save the actual value
transaction.local_status = status
# Now map our local status to the global status codes
if operation == AMAZON_OPERATION_TYPE_PAY:
if status == AMAZON_IPN_STATUS_SUCCESS:
transaction.status = TRANSACTION_STATUS_COMPLETE_PRIMARY
elif status == AMAZON_IPN_STATUS_PENDING:
if transaction.status == TRANSACTION_STATUS_CREATED:
#
# Per the amazon documentation:
# If your IPN receiving service is down for some time, it is possible that our retry mechanism will deliver the IPNs out of order.
# If you receive an IPN for TransactionStatus (IPN), as SUCCESS or FAILURE or RESERVED,
# then after that time ignore any IPN that gives the PENDING status for the transaction
#
transaction.status = TRANSACTION_STATUS_PENDING
else:
transaction.status = TRANSACTION_STATUS_ERROR
elif operation == AMAZON_OPERATION_TYPE_REFUND:
if status == AMAZON_IPN_STATUS_SUCCESS:
transaction.status = TRANSACTION_STATUS_COMPLETE_PRIMARY
else:
transaction.status = TRANSACTION_STATUS_ERROR
elif operation == AMAZON_OPERATION_TYPE_CANCEL:
if status == AMAZON_IPN_STATUS_SUCCESS:
transaction.status = TRANSACTION_STATUS_COMPLETE_PRIMARY
else:
transaction.status = TRANSACTION_STATUS_ERROR
elif type == AMAZON_NOTIFICATION_TYPE_CANCEL:
#
# The cancel IPN does not have a transaction ID or transaction status, so make them up
#
transaction.local_status = AMAZON_IPN_STATUS_CANCELED
transaction.status = TRANSACTION_STATUS_CANCELED
status = AMAZON_IPN_STATUS_CANCELED
transaction.save()
#
# This is currently not done in paypal land, but log this IPN since the amazon IPN has good info
#
PaymentResponse.objects.create(api="IPN",
correlation_id = transactionId,
timestamp = date,
info = str(request.POST),
status=status,
transaction=transaction)
return HttpResponse("Complete")
except:
traceback.print_exc()
return HttpResponseForbidden()
def amazonPaymentReturn(request):
'''
This is the complete view called after the co-branded API completes. It is called whenever the user
approves a preapproval or a pledge.
'''
try:
@ -110,7 +216,7 @@ def amazonPaymentReturn(request):
if status == AMAZON_STATUS_SUCCESS_ABT or status == AMAZON_STATUS_SUCCESS_ACH or status == AMAZON_STATUS_SUCCESS_CREDIT:
# The above status code are unique to the return URL and are different than the pay API codes
# Store the token
# Store the token, we need this for the IPN.
transaction.pay_key = token
#
@ -119,24 +225,14 @@ def amazonPaymentReturn(request):
#
e = Execute(transaction=transaction)
transaction.local_status = e.status
if e.status == AMAZON_STATUS_SUCCESS:
transaction.status = TRANSACTION_STATUS_COMPLETE_PRIMARY
elif e.status == AMAZON_STATUS_PENDING:
# Amazon leaves CC transactions pending until we get the IPN
transaction.status = TRANSACTION_STATUS_PENDING
else:
transaction.status = TRANSACTION_STATUS_ERROR
if e.success() and not e.error():
# Success case, save the ID
# Success case, save the ID. Our IPN will update the status
print "Amazon Execute returned succesfully"
else:
print "Amazon Execute returned an error, status %s" % e.status
# Failure case
logging.error("Amazon payment execution failed: ")
logging.error(e.envelope())
transaction.status = TRANSACTION_STATUS_ERROR
# Log the pay transaction
r = PaymentResponse.objects.create(api="Pay",
@ -147,6 +243,9 @@ def amazonPaymentReturn(request):
transaction=transaction)
else:
# We may never see an IPN, set the status here
logging.error("Amazon payment authorization failed: ")
logging.error(request.GET)
transaction.status = AMAZON_STATUS_FAILURE
@ -165,6 +264,7 @@ def amazonPaymentReturn(request):
transaction.pay_key = token
else:
# We may never see an IPN, set the status here
transaction.status = TRANSACTION_STATUS_ERROR
# Log the trasaction
@ -202,16 +302,6 @@ class AmazonRequest:
def success(self):
print "CALLING SUCCESS"
if self.status:
#
# process the boto response if we have one. The status codes here are only boto response codes, not
# return_url codes
#
if self.status == AMAZON_STATUS_SUCCESS or self.status == AMAZON_STATUS_PENDING:
return True
else:
return False
if self.errorMessage:
return False
else:
@ -293,6 +383,7 @@ class Pay( AmazonRequest ):
print "Amazon PURCHASE url request data: %s" % data
self.url = self.connection.make_url(return_url, "Test Payment", "MultiUse", str(amount), **data)
print "Amazon PURCHASE url was: %s" % self.url
except:
@ -327,6 +418,11 @@ class Preapproval(Pay):
class Execute(AmazonRequest):
'''
The Execute function sends an existing token(generated via the URL from the pay operation), and collects
the money.
'''
def __init__(self, transaction=None):
try:
@ -350,12 +446,23 @@ class Execute(AmazonRequest):
transactionDate=None,
reserve=False)
print "Amazon EXECUTE response was: %s" % self.raw_response
#
# BUGBUG:
# The boto FPS library throws an exception if an error is generated, we need to do a better
# job of reporting the error when this occurs
#
print "Amazon EXECUTE response was: %s" % self.raw_response
self.response = self.raw_response[0]
print "RESPONSE: %s" % self.response
self.status = self.response.TransactionStatus
print "STATUS: %s" % self.status
#
# For amazon, the transactionID is per transaction, not per receiver. For now we will store it in the preapproval key field
# so we can use it to refund or get status later
#
transaction.preapproval_key = self.response.TransactionId
except:
traceback.print_exc()
@ -367,9 +474,14 @@ class Execute(AmazonRequest):
def key(self):
# IN paypal land, our key is updated from a preapproval to a pay key here, just return the existing key
return self.transaction.pay_key
class Finish(AmazonRequest):
'''
The Finish function handles the secondary receiver in a chained payment. Currently not implemented
for amazon
'''
def __init__(self, transaction):
try:
@ -381,24 +493,104 @@ class Finish(AmazonRequest):
self.errorMessage = "Error: Server Error"
class PaymentDetails(AmazonRequest):
def __init__(self, transaction=None):
def __init__(self, transaction=None):
try:
print "Payment Details"
try:
# Use the boto class top open a connection
self.connection = FPSConnection(settings.FPS_ACCESS_KEY, settings.FPS_SECRET_KEY)
self.transaction = transaction
if not transaction.preapproval_key:
# This is where we store the transaction ID
self.errorMessage = "No Valid Transaction ID"
return
#
# We need to reference the transaction ID here, this is stored in the preapproval_key as this
# field is not used for amazon
#
self.raw_response = self.connection.get_transaction_status(transaction.preapproval_key)
print "Amazon TRANSACTION STATUS response was: %s" % self.raw_response
except:
self.errorMessage = "Error: ServerError"
traceback.print_exc()
self.response = self.raw_response[0]
print "RESPONSE: %s" % self.response
#
# Now we need to build values to match the paypal response.
# The two we need are status and and array of transactions.
#
# Check our status codes, note that these are different than the IPN status codes
self.local_status = self.response.StatusCode
self.message = self.response.StatusMessage
if self.local_status == 'Canceled':
self.status = TRANSACTION_STATUS_CANCELED
elif self.local_status == 'Success':
self.status = TRANSACTION_STATUS_COMPLETE_PRIMARY
elif self.local_status == 'PendingNetworkResponse' or self.local_status == 'PendingVerification':
self.status = TRANSACTION_STATUS_PENDING
elif self.local_status == 'TransactionDenied':
self.status = TRANSACTION_STATUS_FAILED
else:
self.status = TRANSACTION_STATUS_ERROR
# Amazon does not support receivers at this point
self.transactions = []
print self.status
except:
self.errorMessage = "Error: ServerError"
traceback.print_exc()
class CancelPreapproval(AmazonRequest):
'''
Cancels an exisiting token. The current boto FPS library does not directly support
the CancelToken API, just the Cancel API(for real money in-flight or reserved).
'''
def __init__(self, transaction):
try:
print "Cancel Preapproval"
# Use the boto class top open a connection
self.connection = FPSConnection(settings.FPS_ACCESS_KEY, settings.FPS_SECRET_KEY)
self.transaction = transaction
params = {}
params['TokenId'] = transaction.pay_key
params['ReasonText'] = "Cancel Reason"
fps_response = self.connection.make_request("CancelToken", params)
body = fps_response.read()
print body
if(fps_response.status == 200):
rs = ResultSet()
h = handler.XmlHandler(rs, self)
xml.sax.parseString(body, h)
if rs:
self.raw_response = rs
self.response = self.raw_response[0]
self.status = self.response.TransactionStatus
self.errorMessage = None
else:
#
# Set an error message and failure status for
# our success() and error() functions
#
self.status = AMAZON_STATUS_FAILURE
self.errorMessage = "%s - %s" % (fps_response.reason, body)
except:
traceback.print_exc()
@ -410,7 +602,27 @@ class RefundPayment(AmazonRequest):
def __init__(self, transaction):
try:
print "Refund Payment"
# Use the boto class top open a connection
self.connection = FPSConnection(settings.FPS_ACCESS_KEY, settings.FPS_SECRET_KEY)
self.transaction = transaction
if not transaction.preapproval_key:
# This is where we store the transaction ID
self.errorMessage = "No Valid Transaction ID"
return
#
# We need to reference the transaction ID here, this is stored in the preapproval_key as this
# field is not used for amazon
#
self.raw_response = self.connection.refund(transaction.secret, transaction.preapproval_key)
print "Amazon TRANSACTION REFUND response was: %s" % self.raw_response
self.response = self.raw_response[0]
print "RESPONSE: %s" % self.response
self.status = self.response.TransactionStatus
print "STATUS: %s" % self.status
except:
traceback.print_exc()