def setUp(self): self.ipn_record = IPNRecord( transaction_id=1, data='a=1', ) self.ipn_record.save() self.order = Order( donation=100, status=Order.SHOPPING_CART, ) self.order.save() self.product = Product( name='Test Product', slug='test-product', price=500, tax_deductible=50, min_quantity=0, max_quantity=10, current_quantity=5, status=Product.FOR_SALE, ) self.product.save() self.product_in_order = ProductInOrder( order=self.order, product=self.product, quantity=1, ) self.product_in_order.save()
class TestPayPalIPN(TestCase): def setUp(self): self.ipn_record = IPNRecord( transaction_id=1, data='a=1', ) self.ipn_record.save() self.order = Order( donation=100, status=Order.SHOPPING_CART, ) self.order.save() self.product = Product( name='Test Product', slug='test-product', price=500, tax_deductible=50, min_quantity=0, max_quantity=10, current_quantity=5, status=Product.FOR_SALE, ) self.product.save() self.product_in_order = ProductInOrder( order=self.order, product=self.product, quantity=1, ) self.product_in_order.save() def test_ipn_error_conditions(self): # Anything besides POST gets a 404 response = self.client.get('/paypal/ipn/') self.assertEqual(response.status_code, 404) # Can't verify message with PayPal response = self.client.post('/paypal/ipn/', {}) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok (unverified)') # Message not for our business response = self.client.post('/paypal/ipn/', { '_fake_paypal_verification': settings.SECRET_KEY, 'business': '*****@*****.**', }) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok (wrong business)') # No transaction ID provided response = self.client.post('/paypal/ipn/', { '_fake_paypal_verification': settings.SECRET_KEY, 'business': settings.PAYPAL_BUSINESS, }) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok (no id)') # Duplicate IPN message received response = self.client.post('/paypal/ipn/', { '_fake_paypal_verification': settings.SECRET_KEY, 'business': settings.PAYPAL_BUSINESS, 'txn_id': '1', }) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok (duplicate)') # Status is not Completed response = self.client.post('/paypal/ipn/', { '_fake_paypal_verification': settings.SECRET_KEY, 'business': settings.PAYPAL_BUSINESS, 'txn_id': '2', 'payment_status': 'Waiting', }) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok (not completed)') # No matching order response = self.client.post('/paypal/ipn/', { '_fake_paypal_verification': settings.SECRET_KEY, 'business': settings.PAYPAL_BUSINESS, 'txn_id': '3', 'payment_status': 'Completed', }) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok (no order)') # Payment for wrong amount response = self.client.post('/paypal/ipn/', { '_fake_paypal_verification': settings.SECRET_KEY, 'business': settings.PAYPAL_BUSINESS, 'txn_id': '4', 'payment_status': 'Completed', 'invoice': self.order.invoice_id, 'mc_gross': '1.00', }) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok (wrong amount)') def test_ipn_success_conditions(self): # Order marked as paid response = self.client.post('/paypal/ipn/', { '_fake_paypal_verification': settings.SECRET_KEY, 'business': settings.PAYPAL_BUSINESS, 'txn_id': '5', 'payment_status': 'Completed', 'invoice': self.order.invoice_id, 'mc_gross': '646.25', }) order = Order.objects.get(pk=self.order.id) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'Ok') self.assertEqual(order.status, Order.PAYMENT_CONFIRMED) record = IPNRecord.objects.get(transaction_id='5') self.assertTrue(record.data)
def paypal_ipn(request): """Handles PayPal IPN notifications.""" if not request.method == 'POST': logging.warning('IPN view hit but not POSTed to. Attackers?') raise Http404 paypal_url = settings.PAYPAL_POST_URL # Ack and verify the message with paypal params = request.POST.copy() if params.get('_fake_paypal_verification') == settings.SECRET_KEY: pass else: params['cmd'] = '_notify-validate' req = urllib2.Request(paypal_url, urllib.urlencode(params)) req.add_header('Content-type', 'application/x-www-form-urlencoded') try: response = urllib2.urlopen(req) except urllib2.URLError: logging.error('Unable to contact PayPal to ack the message.') raise Http404 # Let PayPal resend later and try again if response.code != 200 or response.readline() != 'VERIFIED': logging.warning('IPN view could not verify the message with ' 'PayPal. Original: %r' % request.POST) return HttpResponse('Ok (unverified)') params = request.POST.copy() # Verify that the message is for our business business = params.get('business') if business and business != settings.PAYPAL_BUSINESS: logging.warning('IPN received for a random business. Weird.') return HttpResponse('Ok (wrong business)') # Save the notification and ensure it's not a duplicate transaction_id = params.get('txn_id') if not transaction_id: logging.warning('No transaction id provided.') return HttpResponse('Ok (no id)') try: record = IPNRecord(transaction_id=transaction_id, data=repr(params)) record.save() except IntegrityError: logging.warning('IPN was duplicate. Probably nothing to worry about.') return HttpResponse('Ok (duplicate)') # Verify that the status is Completed if params.get('payment_status') != 'Completed': logging.warning('Status not completed. Taking no action.') return HttpResponse('Ok (not completed)') # Verify that the amount paid matches the cart amount invoice_id = params.get('invoice') try: order = Order.objects.get(invoice_id=invoice_id) except Order.DoesNotExist: logging.warning('Could not find corresponding Order.') return HttpResponse('Ok (no order)') order_total = Decimal(str(order.get_as_cart()['total'])).quantize(Decimal('0.01')) paypal_total = Decimal(str(params['mc_gross'])).quantize(Decimal('0.01')) if order_total != paypal_total: logging.warning('PayPal payment amount (%.2f) did not match order ' 'amount (%.2f)!' % (paypal_total, order_total)) order.status = Order.PROCESSING_ERROR order.save() return HttpResponse('Ok (wrong amount)') # Take action! Save the details of the order order.status = Order.PAYMENT_CONFIRMED order.user_email = params.get('payer_email', '') order.user_firstname = params.get('first_name', '') order.user_lastname = params.get('last_name', '') order.user_shiptoname = params.get('address_name', '') order.user_shiptostreet = params.get('address_street', '') order.user_shiptostreet2 = '' order.user_shiptocity = params.get('address_city', '') order.user_shiptostate = params.get('address_state', '') order.user_shiptozip = params.get('address_zip', '') order.user_shiptocountrycode = params.get('address_country_code', '') order.user_shiptophonenum = params.get('contact_phone', '') order.paypal_transactionid = params.get('txn_id', '') order.paypal_paymenttype = params.get('payment_type', '') try: order.paypal_ordertime = datetime.strptime(params.get('payment_date'), '%H:%M:%S %b %d, %Y %Z') except ValueError: order.paypal_ordertime = None order.paypal_amt = params.get('mc_gross') order.paypal_feeamt = params.get('mc_fee') order.paypal_paymentstatus = params.get('payment_status', '') order.paypal_notetext = '' order.paypal_details_dump = params.urlencode() order.save() # Increment the number of products sold for product_in_order in order.productinorder_set.all(): product = product_in_order.product product.current_quantity += product_in_order.quantity product.save() # Create a confirmation email to be sent context = {'order': order} message = Message() message.to_email = order.user_email message.from_email = settings.DEFAULT_FROM_EMAIL message.subject = render_to_string('mail/order_confirmation_subject.txt', context) message.body_text = render_to_string('mail/order_confirmation.txt', context) message.body_html = render_to_string('mail/order_confirmation.html', context) message.save() logging.info('PayPal payment recorded successfully.') return HttpResponse('Ok')