15
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
17
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
17
18
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
18
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19
20
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
21
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
28
from boto import handler
29
26
from boto.connection import AWSQueryConnection
30
from boto.resultset import ResultSet
31
from boto.exception import FPSResponseError
32
from boto.fps.response import FPSResponse
27
from boto.fps.exception import ResponseErrorFactory
28
from boto.fps.response import ResponseFactory
29
import boto.fps.response
31
__all__ = ['FPSConnection']
33
decorated_attrs = ('action', 'response')
36
def add_attrs_from(func, to):
37
for attr in decorated_attrs:
38
setattr(to, attr, getattr(func, attr, None))
42
def complex_amounts(*fields):
44
def wrapper(self, *args, **kw):
45
for field in filter(kw.has_key, fields):
46
amount = kw.pop(field)
47
kw[field + '.Value'] = getattr(amount, 'Value', str(amount))
48
kw[field + '.CurrencyCode'] = getattr(amount, 'CurrencyCode',
50
return func(self, *args, **kw)
51
wrapper.__doc__ = "{0}\nComplex Amounts: {1}".format(func.__doc__,
53
return add_attrs_from(func, to=wrapper)
57
def requires(*groups):
61
def wrapper(*args, **kw):
62
hasgroup = lambda x: len(x) == len(filter(kw.has_key, x))
63
if 1 != len(filter(hasgroup, groups)):
64
message = ' OR '.join(['+'.join(g) for g in groups])
65
message = "{0} requires {1} argument(s)" \
66
"".format(getattr(func, 'action', 'Method'), message)
67
raise KeyError(message)
68
return func(*args, **kw)
69
message = ' OR '.join(['+'.join(g) for g in groups])
70
wrapper.__doc__ = "{0}\nRequired: {1}".format(func.__doc__,
72
return add_attrs_from(func, to=wrapper)
76
def needs_caller_reference(func):
78
def wrapper(*args, **kw):
79
kw.setdefault('CallerReference', uuid.uuid4())
80
return func(*args, **kw)
81
wrapper.__doc__ = "{0}\nUses CallerReference, defaults " \
82
"to uuid.uuid4()".format(func.__doc__)
83
return add_attrs_from(func, to=wrapper)
89
action = ''.join(api or map(str.capitalize, func.func_name.split('_')))
90
response = ResponseFactory(action)
91
if hasattr(boto.fps.response, action + 'Response'):
92
response = getattr(boto.fps.response, action + 'Response')
94
def wrapper(self, *args, **kw):
95
return func(self, action, response, *args, **kw)
96
wrapper.action, wrapper.response = action, response
97
wrapper.__doc__ = "FPS {0} API call\n{1}".format(action,
34
103
class FPSConnection(AWSQueryConnection):
36
105
APIVersion = '2010-08-28'
38
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
39
is_secure=True, port=None, proxy=None, proxy_port=None,
40
proxy_user=None, proxy_pass=None,
41
host='fps.sandbox.amazonaws.com', debug=0,
42
https_connection_factory=None, path="/"):
43
AWSQueryConnection.__init__(self, aws_access_key_id,
44
aws_secret_access_key,
45
is_secure, port, proxy, proxy_port,
46
proxy_user, proxy_pass, host, debug,
47
https_connection_factory, path)
106
ResponseError = ResponseErrorFactory
109
def __init__(self, *args, **kw):
110
self.currencycode = kw.pop('CurrencyCode', self.currencycode)
111
kw.setdefault('host', 'fps.sandbox.amazonaws.com')
112
AWSQueryConnection.__init__(self, *args, **kw)
49
114
def _required_auth_capability(self):
52
def install_payment_instruction(self, instruction,
53
token_type="Unrestricted",
56
InstallPaymentInstruction
57
instruction: The PaymentInstruction to send, for example:
59
MyRole=='Caller' orSay 'Roles do not match';
61
token_type: Defaults to "Unrestricted"
62
transaction_id: Defaults to a new ID
65
if(transaction_id == None):
66
transaction_id = uuid.uuid4()
68
params['PaymentInstruction'] = instruction
69
params['TokenType'] = token_type
70
params['CallerReference'] = transaction_id
71
response = self.make_request("InstallPaymentInstruction", params)
74
def install_caller_instruction(self, token_type="Unrestricted",
78
This will install a new caller_token into the FPS section.
79
This should really only be called to regenerate the caller token.
81
response = self.install_payment_instruction("MyRole=='Caller';",
82
token_type=token_type,
83
transaction_id=transaction_id)
84
body = response.read()
85
if(response.status == 200):
87
h = handler.XmlHandler(rs, self)
88
xml.sax.parseString(body, h)
89
caller_token = rs.TokenId
91
boto.config.save_system_option("FPS", "caller_token",
94
boto.config.save_user_option("FPS", "caller_token",
98
raise FPSResponseError(response.status, response.reason, body)
100
def install_recipient_instruction(self, token_type="Unrestricted",
101
transaction_id=None):
103
Set us up as a Recipient
104
This will install a new caller_token into the FPS section.
105
This should really only be called to regenerate the recipient token.
107
response = self.install_payment_instruction("MyRole=='Recipient';",
108
token_type=token_type,
109
transaction_id=transaction_id)
110
body = response.read()
111
if(response.status == 200):
113
h = handler.XmlHandler(rs, self)
114
xml.sax.parseString(body, h)
115
recipient_token = rs.TokenId
117
boto.config.save_system_option("FPS", "recipient_token",
120
boto.config.save_user_option("FPS", "recipient_token",
123
return recipient_token
125
raise FPSResponseError(response.status, response.reason, body)
127
def make_marketplace_registration_url(self, returnURL, pipelineName,
128
maxFixedFee=0.0, maxVariableFee=0.0,
129
recipientPaysFee=True, **params):
131
Generate the URL with the signature required for signing up a recipient
133
# use the sandbox authorization endpoint if we're using the
134
# sandbox for API calls.
135
endpoint_host = 'authorize.payments.amazon.com'
136
if 'sandbox' in self.host:
137
endpoint_host = 'authorize.payments-sandbox.amazon.com'
138
base = "/cobranded-ui/actions/start"
140
params['callerKey'] = str(self.aws_access_key_id)
141
params['returnURL'] = str(returnURL)
142
params['pipelineName'] = str(pipelineName)
143
params['maxFixedFee'] = str(maxFixedFee)
144
params['maxVariableFee'] = str(maxVariableFee)
145
params['recipientPaysFee'] = str(recipientPaysFee)
146
params["signatureMethod"] = 'HmacSHA256'
147
params["signatureVersion"] = '2'
149
if(not params.has_key('callerReference')):
150
params['callerReference'] = str(uuid.uuid4())
153
for k in sorted(params.keys()):
154
parts += "&%s=%s" % (k, urllib.quote(params[k], '~'))
156
canonical = '\n'.join(['GET',
157
str(endpoint_host).lower(),
161
signature = self._auth_handler.sign_string(canonical)
162
params["signature"] = signature
165
for k in sorted(params.keys()):
166
urlsuffix += "&%s=%s" % (k, urllib.quote(params[k], '~'))
167
urlsuffix = urlsuffix[1:] # strip the first &
169
fmt = "https://%(endpoint_host)s%(base)s?%(urlsuffix)s"
174
def make_url(self, returnURL, paymentReason, pipelineName,
175
transactionAmount, **params):
177
Generate the URL with the signature required for a transaction
179
# use the sandbox authorization endpoint if we're using the
180
# sandbox for API calls.
181
endpoint_host = 'authorize.payments.amazon.com'
182
if 'sandbox' in self.host:
183
endpoint_host = 'authorize.payments-sandbox.amazon.com'
184
base = "/cobranded-ui/actions/start"
186
params['callerKey'] = str(self.aws_access_key_id)
187
params['returnURL'] = str(returnURL)
188
params['paymentReason'] = str(paymentReason)
189
params['pipelineName'] = pipelineName
190
params['transactionAmount'] = transactionAmount
191
params["signatureMethod"] = 'HmacSHA256'
192
params["signatureVersion"] = '2'
194
if(not params.has_key('callerReference')):
195
params['callerReference'] = str(uuid.uuid4())
198
for k in sorted(params.keys()):
199
parts += "&%s=%s" % (k, urllib.quote(params[k], '~'))
201
canonical = '\n'.join(['GET',
202
str(endpoint_host).lower(),
206
signature = self._auth_handler.sign_string(canonical)
207
params["signature"] = signature
210
for k in sorted(params.keys()):
211
urlsuffix += "&%s=%s" % (k, urllib.quote(params[k], '~'))
212
urlsuffix = urlsuffix[1:] # strip the first &
214
fmt = "https://%(endpoint_host)s%(base)s?%(urlsuffix)s"
218
def pay(self, transactionAmount, senderTokenId,
219
recipientTokenId=None,
220
chargeFeeTo="Recipient",
221
callerReference=None, senderReference=None, recipientReference=None,
222
senderDescription=None, recipientDescription=None,
223
callerDescription=None, metadata=None,
224
transactionDate=None, reserve=False):
226
Make a payment transaction. You must specify the amount.
227
This can also perform a Reserve request if 'reserve' is set to True.
230
params['SenderTokenId'] = senderTokenId
231
# this is for 2010-08-28 specification
232
params['TransactionAmount.Value'] = str(transactionAmount)
233
params['TransactionAmount.CurrencyCode'] = "USD"
234
params['ChargeFeeTo'] = chargeFeeTo
237
params['RecipientTokenId'] = (
238
recipientTokenId if recipientTokenId is not None
239
else boto.config.get("FPS", "recipient_token")
241
if(transactionDate != None):
242
params['TransactionDate'] = transactionDate
243
if(senderReference != None):
244
params['SenderReference'] = senderReference
245
if(recipientReference != None):
246
params['RecipientReference'] = recipientReference
247
if(senderDescription != None):
248
params['SenderDescription'] = senderDescription
249
if(recipientDescription != None):
250
params['RecipientDescription'] = recipientDescription
251
if(callerDescription != None):
252
params['CallerDescription'] = callerDescription
253
if(metadata != None):
254
params['MetaData'] = metadata
255
if(callerReference == None):
256
callerReference = uuid.uuid4()
257
params['CallerReference'] = callerReference
263
response = self.make_request(action, params)
264
body = response.read()
265
if(response.status == 200):
266
rs = ResultSet([("%sResponse" %action, FPSResponse)])
267
h = handler.XmlHandler(rs, self)
268
xml.sax.parseString(body, h)
271
raise FPSResponseError(response.status, response.reason, body)
273
def get_transaction_status(self, transactionId):
275
Returns the status of a given transaction.
278
params['TransactionId'] = transactionId
280
response = self.make_request("GetTransactionStatus", params)
281
body = response.read()
282
if(response.status == 200):
283
rs = ResultSet([("GetTransactionStatusResponse", FPSResponse)])
284
h = handler.XmlHandler(rs, self)
285
xml.sax.parseString(body, h)
288
raise FPSResponseError(response.status, response.reason, body)
290
def cancel(self, transactionId, description=None):
292
Cancels a reserved or pending transaction.
295
params['TransactionId'] = transactionId
296
if(description != None):
297
params['description'] = description
299
response = self.make_request("Cancel", params)
300
body = response.read()
301
if(response.status == 200):
302
rs = ResultSet([("CancelResponse", FPSResponse)])
303
h = handler.XmlHandler(rs, self)
304
xml.sax.parseString(body, h)
307
raise FPSResponseError(response.status, response.reason, body)
309
def settle(self, reserveTransactionId, transactionAmount=None):
311
Charges for a reserved payment.
314
params['ReserveTransactionId'] = reserveTransactionId
315
if(transactionAmount != None):
316
params['TransactionAmount.Value'] = transactionAmount
317
params['TransactionAmount.CurrencyCode'] = "USD"
319
response = self.make_request("Settle", params)
320
body = response.read()
321
if(response.status == 200):
322
rs = ResultSet([("SettleResponse", FPSResponse)])
323
h = handler.XmlHandler(rs, self)
324
xml.sax.parseString(body, h)
327
raise FPSResponseError(response.status, response.reason, body)
329
def refund(self, callerReference, transactionId, refundAmount=None,
330
callerDescription=None):
332
Refund a transaction. This refunds the full amount by default
333
unless 'refundAmount' is specified.
336
params['CallerReference'] = callerReference
337
params['TransactionId'] = transactionId
338
if(refundAmount != None):
339
params['RefundAmount.Value'] = refundAmount
340
params['RefundAmount.CurrencyCode'] = "USD"
341
if(callerDescription != None):
342
params['CallerDescription'] = callerDescription
344
response = self.make_request("Refund", params)
345
body = response.read()
346
if(response.status == 200):
347
rs = ResultSet([("RefundResponse", FPSResponse)])
348
h = handler.XmlHandler(rs, self)
349
xml.sax.parseString(body, h)
352
raise FPSResponseError(response.status, response.reason, body)
354
def get_recipient_verification_status(self, recipientTokenId):
356
Test that the intended recipient has a verified Amazon Payments account.
359
params['RecipientTokenId'] = recipientTokenId
361
response = self.make_request("GetRecipientVerificationStatus", params)
362
body = response.read()
363
if(response.status == 200):
365
h = handler.XmlHandler(rs, self)
366
xml.sax.parseString(body, h)
369
raise FPSResponseError(response.status, response.reason, body)
371
def get_token_by_caller_reference(self, callerReference):
373
Returns details about the token specified by 'CallerReference'.
376
params['CallerReference'] = callerReference
378
response = self.make_request("GetTokenByCaller", params)
379
body = response.read()
380
if(response.status == 200):
382
h = handler.XmlHandler(rs, self)
383
xml.sax.parseString(body, h)
386
raise FPSResponseError(response.status, response.reason, body)
388
def get_token_by_caller_token(self, tokenId):
390
Returns details about the token specified by 'TokenId'.
393
params['TokenId'] = tokenId
395
response = self.make_request("GetTokenByCaller", params)
396
body = response.read()
397
if(response.status == 200):
399
h = handler.XmlHandler(rs, self)
400
xml.sax.parseString(body, h)
403
raise FPSResponseError(response.status, response.reason, body)
405
def verify_signature(self, end_point_url, http_parameters):
407
UrlEndPoint = end_point_url,
408
HttpParameters = http_parameters,
410
response = self.make_request("VerifySignature", params)
411
body = response.read()
412
if(response.status != 200):
413
raise FPSResponseError(response.status, response.reason, body)
414
rs = ResultSet([("VerifySignatureResponse", FPSResponse)])
415
h = handler.XmlHandler(rs, self)
416
xml.sax.parseString(body, h)
117
@needs_caller_reference
118
@complex_amounts('SettlementAmount')
119
@requires(['CreditInstrumentId', 'SettlementAmount.Value',
120
'SenderTokenId', 'SettlementAmount.CurrencyCode'])
122
def settle_debt(self, action, response, **kw):
124
Allows a caller to initiate a transaction that atomically transfers
125
money from a sender's payment instrument to the recipient, while
126
decreasing corresponding debt balance.
128
return self.get_object(action, kw, response)
130
@requires(['TransactionId'])
132
def get_transaction_status(self, action, response, **kw):
134
Gets the latest status of a transaction.
136
return self.get_object(action, kw, response)
138
@requires(['StartDate'])
140
def get_account_activity(self, action, response, **kw):
142
Returns transactions for a given date range.
144
return self.get_object(action, kw, response)
146
@requires(['TransactionId'])
148
def get_transaction(self, action, response, **kw):
150
Returns all details of a transaction.
152
return self.get_object(action, kw, response)
155
def get_outstanding_debt_balance(self, action, response):
157
Returns the total outstanding balance for all the credit instruments
158
for the given creditor account.
160
return self.get_object(action, {}, response)
162
@requires(['PrepaidInstrumentId'])
164
def get_prepaid_balance(self, action, response, **kw):
166
Returns the balance available on the given prepaid instrument.
168
return self.get_object(action, kw, response)
171
def get_total_prepaid_liability(self, action, response):
173
Returns the total liability held by the given account corresponding to
174
all the prepaid instruments owned by the account.
176
return self.get_object(action, {}, response)
179
def get_account_balance(self, action, response):
181
Returns the account balance for an account in real time.
183
return self.get_object(action, {}, response)
185
@needs_caller_reference
186
@requires(['PaymentInstruction', 'TokenType'])
188
def install_payment_instruction(self, action, response, **kw):
190
Installs a payment instruction for caller.
192
return self.get_object(action, kw, response)
194
@needs_caller_reference
195
@requires(['returnURL', 'pipelineName'])
196
def cbui_url(self, **kw):
198
Generate a signed URL for the Co-Branded service API given arguments as
201
sandbox = 'sandbox' in self.host and 'payments-sandbox' or 'payments'
202
endpoint = 'authorize.{0}.amazon.com'.format(sandbox)
203
base = '/cobranded-ui/actions/start'
205
validpipelines = ('SingleUse', 'MultiUse', 'Recurring', 'Recipient',
206
'SetupPrepaid', 'SetupPostpaid', 'EditToken')
207
assert kw['pipelineName'] in validpipelines, "Invalid pipelineName"
209
'signatureMethod': 'HmacSHA256',
210
'signatureVersion': '2',
212
kw.setdefault('callerKey', self.aws_access_key_id)
214
safestr = lambda x: x is not None and str(x) or ''
215
safequote = lambda x: urllib.quote(safestr(x), safe='~')
216
payload = sorted([(k, safequote(v)) for k, v in kw.items()])
218
encoded = lambda p: '&'.join([k + '=' + v for k, v in p])
219
canonical = '\n'.join(['GET', endpoint, base, encoded(payload)])
220
signature = self._auth_handler.sign_string(canonical)
221
payload += [('signature', safequote(signature))]
224
return 'https://{0}{1}?{2}'.format(endpoint, base, encoded(payload))
226
@needs_caller_reference
227
@complex_amounts('TransactionAmount')
228
@requires(['SenderTokenId', 'TransactionAmount.Value',
229
'TransactionAmount.CurrencyCode'])
231
def reserve(self, action, response, **kw):
233
Reserve API is part of the Reserve and Settle API conjunction that
234
serve the purpose of a pay where the authorization and settlement have
237
return self.get_object(action, kw, response)
239
@needs_caller_reference
240
@complex_amounts('TransactionAmount')
241
@requires(['SenderTokenId', 'TransactionAmount.Value',
242
'TransactionAmount.CurrencyCode'])
244
def pay(self, action, response, **kw):
246
Allows calling applications to move money from a sender to a recipient.
248
return self.get_object(action, kw, response)
250
@requires(['TransactionId'])
252
def cancel(self, action, response, **kw):
254
Cancels an ongoing transaction and puts it in cancelled state.
256
return self.get_object(action, kw, response)
258
@complex_amounts('TransactionAmount')
259
@requires(['ReserveTransactionId', 'TransactionAmount.Value',
260
'TransactionAmount.CurrencyCode'])
262
def settle(self, action, response, **kw):
264
The Settle API is used in conjunction with the Reserve API and is used
265
to settle previously reserved transaction.
267
return self.get_object(action, kw, response)
269
@complex_amounts('RefundAmount')
270
@requires(['TransactionId', 'RefundAmount.Value',
271
'CallerReference', 'RefundAmount.CurrencyCode'])
273
def refund(self, action, response, **kw):
275
Refunds a previously completed transaction.
277
return self.get_object(action, kw, response)
279
@requires(['RecipientTokenId'])
281
def get_recipient_verification_status(self, action, response, **kw):
283
Returns the recipient status.
285
return self.get_object(action, kw, response)
287
@requires(['CallerReference'], ['TokenId'])
289
def get_token_by_caller(self, action, response, **kw):
291
Returns the details of a particular token installed by this calling
292
application using the subway co-branded UI.
294
return self.get_object(action, kw, response)
296
@requires(['UrlEndPoint', 'HttpParameters'])
298
def verify_signature(self, action, response, **kw):
300
Verify the signature that FPS sent in IPN or callback urls.
302
return self.get_object(action, kw, response)
305
def get_tokens(self, action, response, **kw):
307
Returns a list of tokens installed on the given account.
309
return self.get_object(action, kw, response)
311
@requires(['TokenId'])
313
def get_token_usage(self, action, response, **kw):
315
Returns the usage of a token.
317
return self.get_object(action, kw, response)
319
@requires(['TokenId'])
321
def cancel_token(self, action, response, **kw):
323
Cancels any token installed by the calling application on its own
326
return self.get_object(action, kw, response)
328
@needs_caller_reference
329
@complex_amounts('FundingAmount')
330
@requires(['PrepaidInstrumentId', 'FundingAmount.Value',
331
'SenderTokenId', 'FundingAmount.CurrencyCode'])
333
def fund_prepaid(self, action, response, **kw):
335
Funds the prepaid balance on the given prepaid instrument.
337
return self.get_object(action, kw, response)
339
@requires(['CreditInstrumentId'])
341
def get_debt_balance(self, action, response, **kw):
343
Returns the balance corresponding to the given credit instrument.
345
return self.get_object(action, kw, response)
347
@needs_caller_reference
348
@complex_amounts('AdjustmentAmount')
349
@requires(['CreditInstrumentId', 'AdjustmentAmount.Value',
350
'AdjustmentAmount.CurrencyCode'])
352
def write_off_debt(self, action, response, **kw):
354
Allows a creditor to write off the debt balance accumulated partially
355
or fully at any time.
357
return self.get_object(action, kw, response)
359
@requires(['SubscriptionId'])
361
def get_transactions_for_subscription(self, action, response, **kw):
363
Returns the transactions for a given subscriptionID.
365
return self.get_object(action, kw, response)
367
@requires(['SubscriptionId'])
369
def get_subscription_details(self, action, response, **kw):
371
Returns the details of Subscription for a given subscriptionID.
373
return self.get_object(action, kw, response)
375
@needs_caller_reference
376
@complex_amounts('RefundAmount')
377
@requires(['SubscriptionId'])
379
def cancel_subscription_and_refund(self, action, response, **kw):
381
Cancels a subscription.
383
message = "If you specify a RefundAmount, " \
384
"you must specify CallerReference."
385
assert not 'RefundAmount.Value' in kw \
386
or 'CallerReference' in kw, message
387
return self.get_object(action, kw, response)
389
@requires(['TokenId'])
391
def get_payment_instruction(self, action, response, **kw):
393
Gets the payment instruction of a token.
395
return self.get_object(action, kw, response)