1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | # This module provides a simple API for Paymentech(c) payments |
---|
5 | # The original code was taken from this web2py issue post |
---|
6 | # http://code.google.com/p/web2py/issues/detail?id=1170 by Adnan Smajlovic |
---|
7 | # |
---|
8 | # Copyright (C) <2012> Alan Etkin <spametki@gmail.com> |
---|
9 | # License: BSD |
---|
10 | # |
---|
11 | |
---|
12 | import sys, httplib, urllib, urllib2 |
---|
13 | from xml.dom.minidom import parseString |
---|
14 | |
---|
15 | # TODO: input validation, test, debugging output |
---|
16 | |
---|
17 | class PaymenTech(object): |
---|
18 | """ |
---|
19 | The base class for connecting to the Paymentech service |
---|
20 | |
---|
21 | Format notes |
---|
22 | ============ |
---|
23 | |
---|
24 | - Credit card expiration date (exp argument) must be of mmyyyy form |
---|
25 | - The amount is an all integers string with two decimal places: |
---|
26 | For example, $2.15 must be formatted as "215" |
---|
27 | |
---|
28 | Point of sale and service options (to be passed on initialization) |
---|
29 | ================================================================== |
---|
30 | |
---|
31 | user |
---|
32 | password |
---|
33 | industry |
---|
34 | message |
---|
35 | bin_code |
---|
36 | merchant |
---|
37 | terminal |
---|
38 | |
---|
39 | (WARNING!: this is False by default) |
---|
40 | development <bool> |
---|
41 | |
---|
42 | (the following arguments have default values) |
---|
43 | target |
---|
44 | host |
---|
45 | api_url |
---|
46 | |
---|
47 | |
---|
48 | Testing |
---|
49 | ======= |
---|
50 | |
---|
51 | As this module consumes webservice methods, it should be tested |
---|
52 | with particular user data with the paymentech development environment |
---|
53 | |
---|
54 | The simplest test would be running something like the following: |
---|
55 | |
---|
56 | from paymentech import PaymenTech |
---|
57 | # Read the basic point of sale argument list required above |
---|
58 | # Remember to use development = True! |
---|
59 | pos_data = {'user': <username>, ...} |
---|
60 | |
---|
61 | # The data arguments are documented in the .charge() method help |
---|
62 | charge_test = {'account': <account>, ...} |
---|
63 | mypayment = PaymentTech(**pos_data) |
---|
64 | result = mypayment.charge(**charge_test) |
---|
65 | |
---|
66 | print "##################################" |
---|
67 | print "# Charge test result #" |
---|
68 | print "##################################" |
---|
69 | print result |
---|
70 | |
---|
71 | ################################################################# |
---|
72 | # Notes for web2py implementations # |
---|
73 | ################################################################# |
---|
74 | |
---|
75 | # A recommended model for handling payments |
---|
76 | |
---|
77 | # Store this constants in a private model file (i.e. 0_private.py) |
---|
78 | |
---|
79 | PAYMENTECH_USER = <str> |
---|
80 | PAYMENTECH_PASSWORD = <str> |
---|
81 | PAYMENTECH_INDUSTRY = <str> |
---|
82 | PAYMENTECH_MESSAGE = <str> |
---|
83 | PAYMENTECH_BIN_CODE= <str> |
---|
84 | PAYMENTECH_MERCHANT = <str> |
---|
85 | PAYMENTECH_terminal = <str> |
---|
86 | DEVELOPMENT = True |
---|
87 | PAYMENTECH_TARGET = <str> |
---|
88 | PAYMENTECH_HOST = <str> |
---|
89 | PAYMENTECH_API_URL = <str> |
---|
90 | |
---|
91 | # The following table would allow passing data with web2py and to |
---|
92 | # update records with the webservice authorization output by using |
---|
93 | # the DAL |
---|
94 | # |
---|
95 | # For example: |
---|
96 | # |
---|
97 | # # Create a PaymenTech instance |
---|
98 | # mypaymentech = paymentech.PaymenTech(user=PAYMENTECH_USER, ...) |
---|
99 | # |
---|
100 | # # Fetch a payment inserted within the app |
---|
101 | # myrow = db.paymentech[<id>] |
---|
102 | # |
---|
103 | # # Send the authorization request to the webservice |
---|
104 | # result = mypaymentech.charge(myrow.as_dict()) |
---|
105 | # |
---|
106 | # # Update the db record with the webservice response |
---|
107 | # myrow.update_record(**result) |
---|
108 | |
---|
109 | db.define_table("paymentech", |
---|
110 | Field("account"), |
---|
111 | Field("exp", comment="Must be of the mmyyyy form"), |
---|
112 | Field("currency_code"), |
---|
113 | Field("currency_exponent"), |
---|
114 | Field("card_sec_val_ind"), |
---|
115 | Field("card_sec_val"), |
---|
116 | Field("avs_zip"), |
---|
117 | Field("avs_address_1"), |
---|
118 | Field("avs_address_2"), |
---|
119 | Field("avs_city"), |
---|
120 | Field("avs_state"), |
---|
121 | Field("avs_phone"), |
---|
122 | Field("avs_country"), |
---|
123 | Field("profile_from_order_ind"), |
---|
124 | Field("profile_order_override_ind"), |
---|
125 | Field("order_id"), |
---|
126 | Field("amount", |
---|
127 | comment="all integers with two decimal digits, \ |
---|
128 | without dot separation"), |
---|
129 | Field("header"), |
---|
130 | Field("status_code"), |
---|
131 | Field("status_message"), |
---|
132 | Field("resp_code"), |
---|
133 | Field("tx_ref_num"), |
---|
134 | format="%(order_id)s") |
---|
135 | |
---|
136 | TODO: add model form validators (for exp date and amount) |
---|
137 | """ |
---|
138 | |
---|
139 | charge_xml = """ |
---|
140 | <?xml version="1.0" encoding="UTF-8"?> |
---|
141 | <Request> |
---|
142 | <NewOrder> |
---|
143 | <OrbitalConnectionUsername>%(user)s</OrbitalConnectionUsername> |
---|
144 | <OrbitalConnectionPassword>%(password)s</OrbitalConnectionPassword> |
---|
145 | <IndustryType>%(industry)s</IndustryType> |
---|
146 | <MessageType>%(message)s</MessageType> |
---|
147 | <BIN>%(bin)s</BIN> |
---|
148 | <MerchantID>%(merchant)s</MerchantID> |
---|
149 | <TerminalID>%(terminal)s</TerminalID> |
---|
150 | <AccountNum>%(account)s</AccountNum> |
---|
151 | <Exp>%(exp)s</Exp> |
---|
152 | <CurrencyCode>%(currency_code)s</CurrencyCode> |
---|
153 | <CurrencyExponent>%(currency_exponent)s</CurrencyExponent> |
---|
154 | <CardSecValInd>%(card_sec_val_ind)s</CardSecValInd> |
---|
155 | <CardSecVal>%(card_sec_val)s</CardSecVal> |
---|
156 | <AVSzip>%(avs_zip)s</AVSzip> |
---|
157 | <AVSaddress1>%(avs_address_1)s</AVSaddress1> |
---|
158 | <AVSaddress2>%(avs_address_2)s</AVSaddress2> |
---|
159 | <AVScity>%(avs_city)s</AVScity> |
---|
160 | <AVSstate>%(avs_state)s</AVSstate> |
---|
161 | <AVSphoneNum>%(avs_phone)s</AVSphoneNum> |
---|
162 | <AVScountryCode>%(avs_country)s</AVScountryCode> |
---|
163 | <CustomerProfileFromOrderInd>%(profile_from_order_ind)s</CustomerProfileFromOrderInd> |
---|
164 | <CustomerProfileOrderOverrideInd>%(profile_order_override_ind)s</CustomerProfileOrderOverrideInd> |
---|
165 | <OrderID>%(order_id)s</OrderID> |
---|
166 | <Amount>%(amount)s</Amount> |
---|
167 | </NewOrder> |
---|
168 | </Request> |
---|
169 | """ |
---|
170 | |
---|
171 | def __init__(self, development=False, user=None, password=None, |
---|
172 | industry=None, message=None, api_url=None, |
---|
173 | bin_code=None, merchant=None, host=None, |
---|
174 | terminal=None, target=None): |
---|
175 | |
---|
176 | # PaymenTech point of sales data |
---|
177 | self.user = user |
---|
178 | self.password = password |
---|
179 | self.industry = industry |
---|
180 | self.message = message |
---|
181 | self.bin_code = bin_code |
---|
182 | self.merchant = merchant |
---|
183 | self.terminal = terminal |
---|
184 | |
---|
185 | # Service options |
---|
186 | self.development = development |
---|
187 | self.target = target |
---|
188 | self.host = host |
---|
189 | self.api_url = api_url |
---|
190 | |
---|
191 | # dev: https://orbitalvar1.paymentech.net/authorize:443 |
---|
192 | # prod: https://orbital1.paymentech.net/authorize |
---|
193 | |
---|
194 | if self.development is False: |
---|
195 | if not self.target: |
---|
196 | # production |
---|
197 | self.target = "https://orbital1.paymentech.net/authorize" |
---|
198 | |
---|
199 | self.host, self.api_url = \ |
---|
200 | urllib2.splithost(urllib2.splittype(self.target)[1]) |
---|
201 | |
---|
202 | else: |
---|
203 | if not self.target: |
---|
204 | # development |
---|
205 | self.target = "https://orbitalvar1.paymentech.net/authorize" |
---|
206 | if not self.host: |
---|
207 | self.host = "orbitalvar1.paymentech.net/authorize:443" |
---|
208 | if not self.api_url: |
---|
209 | self.api_url = "/" |
---|
210 | |
---|
211 | def charge(self, raw=None, **kwargs): |
---|
212 | """ |
---|
213 | Post an XML request to Paymentech |
---|
214 | This is an example of a call with raw xml data: |
---|
215 | |
---|
216 | from paymentech import PaymenTech |
---|
217 | |
---|
218 | # Note: user/password/etc data is not mandatory as it |
---|
219 | # is retrieved from instance attributes (set on init) |
---|
220 | |
---|
221 | pt = PaymenTech(user="<myuser>", |
---|
222 | password="<mypassword>", |
---|
223 | ...) # see basic user in the class help |
---|
224 | result = pt.charge(raw=xml_string) |
---|
225 | |
---|
226 | A better way to make a charge request is to unpack a dict object |
---|
227 | with the operation data: |
---|
228 | |
---|
229 | ... |
---|
230 | # The complete input values are listed below in |
---|
231 | # "Transacion data..." |
---|
232 | |
---|
233 | charge_data = dict(account=<str>, exp=<str mmyyyy>, ...) |
---|
234 | result = pt.charge(**charge_data) |
---|
235 | |
---|
236 | |
---|
237 | Variable xml_string contains all details about the order, |
---|
238 | plus we are sending username/password in there too... |
---|
239 | |
---|
240 | |
---|
241 | Transaction data (to be passed to the charge() method) |
---|
242 | ====================================================== |
---|
243 | |
---|
244 | (Note that it is possible to override the class user, |
---|
245 | pass, etc. passing those arguments to the .charge() method, |
---|
246 | which are documented in the class help) |
---|
247 | |
---|
248 | account |
---|
249 | exp <str mmyyyy> |
---|
250 | currency_code |
---|
251 | currency_exponent |
---|
252 | card_sec_val_ind |
---|
253 | card_sec_val |
---|
254 | avs_zip |
---|
255 | avs_address_1 |
---|
256 | avs_address_2 |
---|
257 | avs_city |
---|
258 | avs_state |
---|
259 | avs_phone |
---|
260 | avs_country |
---|
261 | profile_from_order_ind |
---|
262 | profile_order_override_ind |
---|
263 | order_id |
---|
264 | amount <str> (all integers with two decimal digits, without dot |
---|
265 | separation) |
---|
266 | |
---|
267 | Request header example |
---|
268 | ====================== |
---|
269 | |
---|
270 | Request: sent as POST to https://orbitalvar1.paymentech.net/authorize:443 |
---|
271 | from 127.0.0.1 |
---|
272 | request headers: |
---|
273 | Content-Type: application/PTI45 |
---|
274 | Content-Type: application/PTI46 |
---|
275 | Content-transfer-encoding: text |
---|
276 | Request-number: 1 |
---|
277 | Document-type: Request |
---|
278 | Trace-number: 1234556446 |
---|
279 | <?xml version="1.0" encoding="UTF-8"?> |
---|
280 | """ |
---|
281 | |
---|
282 | # default charge data |
---|
283 | data = dict(user=self.user, password=self.password, |
---|
284 | industry=self.industry, message=self.message, |
---|
285 | bin_code=self.bin_code, merchant=self.merchant, |
---|
286 | terminal=self.terminal, account="", exp="", |
---|
287 | currency_code="", currency_exponent="", |
---|
288 | card_sec_val_ind="", card_sec_val="", avs_zip="", |
---|
289 | avs_address_1="", avs_address_2="", avs_city="", |
---|
290 | avs_state="", avs_phone="", avs_country="", |
---|
291 | profile_from_order_ind="", |
---|
292 | profile_order_override_ind="", order_id="", |
---|
293 | amount="") |
---|
294 | |
---|
295 | result = dict() |
---|
296 | |
---|
297 | # Complete the charge request with the method kwargs |
---|
298 | for k, v in kwargs.iteritems(): |
---|
299 | data[k] = v |
---|
300 | |
---|
301 | status_code = status_message = header = resp_code = \ |
---|
302 | tx_ref_num = order_id = None |
---|
303 | conn = httplib.HTTPS(self.host) |
---|
304 | conn.putrequest('POST', self.api_url) |
---|
305 | |
---|
306 | if self.development: |
---|
307 | content_type = "PTI56" |
---|
308 | else: |
---|
309 | content_type = "PTI46" |
---|
310 | |
---|
311 | if raw is None: |
---|
312 | xml_string = self.charge_xml % data |
---|
313 | else: |
---|
314 | xml_string = raw |
---|
315 | |
---|
316 | conn.putheader("Content-Type", |
---|
317 | "application/%s") % content_type |
---|
318 | conn.putheader("Content-transfer-encoding", "text") |
---|
319 | conn.putheader("Request-number", "1") |
---|
320 | conn.putheader("Content-length", str(len(xml_string))) |
---|
321 | conn.putheader("Document-type", "Request") |
---|
322 | conn.putheader("Trace-number", str(data["order_id"])) |
---|
323 | conn.putheader("MIME-Version", "1.0") |
---|
324 | conn.endheaders() |
---|
325 | conn.send(xml_string) |
---|
326 | |
---|
327 | result["status_code"], result["status_message"], \ |
---|
328 | result["header"] = conn.getreply() |
---|
329 | |
---|
330 | fp = conn.getfile() |
---|
331 | output = fp.read() |
---|
332 | fp.close() |
---|
333 | |
---|
334 | dom = parseString(output) |
---|
335 | result["resp_code"] = \ |
---|
336 | dom.getElementsByTagName('RespCode')[0].firstChild.data |
---|
337 | result["tx_ref_num"] = \ |
---|
338 | dom.getElementsByTagName('TxRefNum')[0].firstChild.data |
---|
339 | result["order_id"] = \ |
---|
340 | dom.getElementsByTagName('CustomerRefNum')[0].firstChild.data |
---|
341 | |
---|
342 | return result |
---|