From f0e59ca6175f6b61cf08f895be563cc69ae50e7a Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 4 Dec 2012 10:56:30 -0600 Subject: [PATCH] paymentech.py --- VERSION | 2 +- gluon/contrib/paymentech.py | 355 ++++++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 gluon/contrib/paymentech.py diff --git a/VERSION b/VERSION index bccc00fc..fccedd3e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.3.0 (2012-12-04 09:01:56) rc1 +Version 2.3.0 (2012-12-04 10:55:53) rc1 diff --git a/gluon/contrib/paymentech.py b/gluon/contrib/paymentech.py new file mode 100644 index 00000000..a372aa77 --- /dev/null +++ b/gluon/contrib/paymentech.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This module provides a simple API for Paymentech(c) payments +# The original code was taken from this web2py issue post +# http://code.google.com/p/web2py/issues/detail?id=1170 +# +# Copyright (C) <2012> Alan Etkin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys, httplib, urllib, urllib2 +from xml.dom.minidom import parseString + +# TODO: input validation, test, debugging output + +class PaymenTech(object): + """ + The base class for connecting to the Paymentech service + + Format notes + ============ + + - Credit card expiration date (exp argument) must be of mmyyyy form + - The amount is an all integers string with two decimal places: + For example, $2.15 must be formatted as "215" + + Point of sale and service options (to be passed on initialization) + ================================================================== + + user + password + industry + message + bin_code + merchant + terminal + + (WARNING!: this is False by default) + development + + (the following arguments have default values) + target + host + api_url + + + Testing + ======= + + As this module consumes webservice methods, it should be tested + with particular user data with the paymentech development environment + + The simplest test would be running something like the following: + + from paymentech import PaymenTech + # Read the basic point of sale argument list required above + # Remember to use development = True! + pos_data = {'user': , ...} + + # The data arguments are documented in the .charge() method help + charge_test = {'account': , ...} + mypayment = PaymentTech(**pos_data) + result = mypayment.charge(**charge_test) + + print "##################################" + print "# Charge test result #" + print "##################################" + print result + + ################################################################# + # Notes for web2py implementations # + ################################################################# + + # A recommended model for handling payments + + # Store this constants in a private model file (i.e. 0_private.py) + + PAYMENTECH_USER = + PAYMENTECH_PASSWORD = + PAYMENTECH_INDUSTRY = + PAYMENTECH_MESSAGE = + PAYMENTECH_BIN_CODE= + PAYMENTECH_MERCHANT = + PAYMENTECH_terminal = + DEVELOPMENT = True + PAYMENTECH_TARGET = + PAYMENTECH_HOST = + PAYMENTECH_API_URL = + + # The following table would allow passing data with web2py and to + # update records with the webservice authorization output by using + # the DAL + # + # For example: + # + # # Create a PaymenTech instance + # mypaymentech = paymentech.PaymenTech(user=PAYMENTECH_USER, ...) + # + # # Fetch a payment inserted within the app + # myrow = db.paymentech[] + # + # # Send the authorization request to the webservice + # result = mypaymentech.charge(myrow.as_dict()) + # + # # Update the db record with the webservice response + # myrow.update_record(**result) + + db.define_table("paymentech", + Field("account"), + Field("exp", comment="Must be of the mmyyyy form"), + Field("currency_code"), + Field("currency_exponent"), + Field("card_sec_val_ind"), + Field("card_sec_val"), + Field("avs_zip"), + Field("avs_address_1"), + Field("avs_address_2"), + Field("avs_city"), + Field("avs_state"), + Field("avs_phone"), + Field("avs_country"), + Field("profile_from_order_ind"), + Field("profile_order_override_ind"), + Field("order_id"), + Field("amount", + comment="all integers with two decimal digits, \ + without dot separation"), + Field("header"), + Field("status_code"), + Field("status_message"), + Field("resp_code"), + Field("tx_ref_num"), + format="%(order_id)s") + + TODO: add model form validators (for exp date and amount) + """ + + charge_xml = """ + + + + %(user)s + %(password)s + %(industry)s + %(message)s + %(bin)s + %(merchant)s + %(terminal)s + %(account)s + %(exp)s + %(currency_code)s + %(currency_exponent)s + %(card_sec_val_ind)s + %(card_sec_val)s + %(avs_zip)s + %(avs_address_1)s + %(avs_address_2)s + %(avs_city)s + %(avs_state)s + %(avs_phone)s + %(avs_country)s + %(profile_from_order_ind)s + %(profile_order_override_ind)s + %(order_id)s + %(amount)s + + + """ + + def __init__(self, development=False, user=None, password=None, + industry=None, message=None, api_url=None, + bin_code=None, merchant=None, host=None, + terminal=None, target=None): + + # PaymenTech point of sales data + self.user = user + self.password = password + self.industry = industry + self.message = message + self.bin_code = bin_code + self.merchant = merchant + self.terminal = terminal + + # Service options + self.development = development + self.target = target + self.host = host + self.api_url = api_url + + # dev: https://orbitalvar1.paymentech.net/authorize:443 + # prod: https://orbital1.paymentech.net/authorize + + if self.development is False: + if not self.target: + # production + self.target = "https://orbital1.paymentech.net/authorize" + + self.host, self.api_url = \ + urllib2.splithost(urllib2.splittype(self.target)[1]) + + else: + if not self.target: + # development + self.target = "https://orbitalvar1.paymentech.net/authorize" + if not self.host: + self.host = "orbitalvar1.paymentech.net/authorize:443" + if not self.api_url: + self.api_url = "/" + + def charge(self, raw=None, **kwargs): + """ + Post an XML request to Paymentech + This is an example of a call with raw xml data: + + from paymentech import PaymenTech + + # Note: user/password/etc data is not mandatory as it + # is retrieved from instance attributes (set on init) + + pt = PaymenTech(user="", + password="", + ...) # see basic user in the class help + result = pt.charge(raw=xml_string) + + A better way to make a charge request is to unpack a dict object + with the operation data: + + ... + # The complete input values are listed below in + # "Transacion data..." + + charge_data = dict(account=, exp=, ...) + result = pt.charge(**charge_data) + + + Variable xml_string contains all details about the order, + plus we are sending username/password in there too... + + + Transaction data (to be passed to the charge() method) + ====================================================== + + (Note that it is possible to override the class user, + pass, etc. passing those arguments to the .charge() method, + which are documented in the class help) + + account + exp + currency_code + currency_exponent + card_sec_val_ind + card_sec_val + avs_zip + avs_address_1 + avs_address_2 + avs_city + avs_state + avs_phone + avs_country + profile_from_order_ind + profile_order_override_ind + order_id + amount (all integers with two decimal digits, without dot + separation) + + Request header example + ====================== + + Request: sent as POST to https://orbitalvar1.paymentech.net/authorize:443 + from 127.0.0.1 + request headers: + Content-Type: application/PTI45 + Content-Type: application/PTI46 + Content-transfer-encoding: text + Request-number: 1 + Document-type: Request + Trace-number: 1234556446 + + """ + + # default charge data + data = dict(user=self.user, password=self.password, + industry=self.industry, message=self.message, + bin_code=self.bin_code, merchant=self.merchant, + terminal=self.terminal, account="", exp="", + currency_code="", currency_exponent="", + card_sec_val_ind="", card_sec_val="", avs_zip="", + avs_address_1="", avs_address_2="", avs_city="", + avs_state="", avs_phone="", avs_country="", + profile_from_order_ind="", + profile_order_override_ind="", order_id="", + amount="") + + result = dict() + + # Complete the charge request with the method kwargs + for k, v in kwargs.iteritems(): + data[k] = v + + status_code = status_message = header = resp_code = \ + tx_ref_num = order_id = None + conn = httplib.HTTPS(self.host) + conn.putrequest('POST', self.api_url) + + if self.development: + content_type = "PTI56" + else: + content_type = "PTI46" + + if raw is None: + xml_string = self.charge_xml % data + else: + xml_string = raw + + conn.putheader("Content-Type", + "application/%s") % content_type + conn.putheader("Content-transfer-encoding", "text") + conn.putheader("Request-number", "1") + conn.putheader("Content-length", str(len(xml_string))) + conn.putheader("Document-type", "Request") + conn.putheader("Trace-number", str(data["order_id"])) + conn.putheader("MIME-Version", "1.0") + conn.endheaders() + conn.send(xml_string) + + result["status_code"], result["status_message"], \ + result["header"] = conn.getreply() + + fp = conn.getfile() + output = fp.read() + fp.close() + + dom = parseString(output) + result["resp_code"] = \ + dom.getElementsByTagName('RespCode')[0].firstChild.data + result["tx_ref_num"] = \ + dom.getElementsByTagName('TxRefNum')[0].firstChild.data + result["order_id"] = \ + dom.getElementsByTagName('CustomerRefNum')[0].firstChild.data + + return result + +