1# Copyright (c) 2012 Andy Davidoff http://www.disruptek.com/ 2# Copyright (c) 2010 Jason R. Coombs http://www.jaraco.com/ 3# Copyright (c) 2008 Chris Moyer http://coredumped.org/ 4# 5# Permission is hereby granted, free of charge, to any person obtaining a 6# copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, dis- 9# tribute, sublicense, and/or sell copies of the Software, and to permit 10# persons to whom the Software is furnished to do so, subject to the fol- 11# lowing conditions: 12# 13# The above copyright notice and this permission notice shall be included 14# in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 18# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22# IN THE SOFTWARE. 23 24import urllib 25import uuid 26from boto.connection import AWSQueryConnection 27from boto.fps.exception import ResponseErrorFactory 28from boto.fps.response import ResponseFactory 29import boto.fps.response 30 31__all__ = ['FPSConnection'] 32 33decorated_attrs = ('action', 'response') 34 35 36def add_attrs_from(func, to): 37 for attr in decorated_attrs: 38 setattr(to, attr, getattr(func, attr, None)) 39 return to 40 41 42def complex_amounts(*fields): 43 def decorator(func): 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', 49 self.currencycode) 50 return func(self, *args, **kw) 51 wrapper.__doc__ = "{0}\nComplex Amounts: {1}".format(func.__doc__, 52 ', '.join(fields)) 53 return add_attrs_from(func, to=wrapper) 54 return decorator 55 56 57def requires(*groups): 58 59 def decorator(func): 60 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__, 71 message) 72 return add_attrs_from(func, to=wrapper) 73 return decorator 74 75 76def needs_caller_reference(func): 77 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) 84 85 86def api_action(*api): 87 88 def decorator(func): 89 action = ''.join(api or map(str.capitalize, func.__name__.split('_'))) 90 response = ResponseFactory(action) 91 if hasattr(boto.fps.response, action + 'Response'): 92 response = getattr(boto.fps.response, action + 'Response') 93 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, 98 func.__doc__) 99 return wrapper 100 return decorator 101 102 103class FPSConnection(AWSQueryConnection): 104 105 APIVersion = '2010-08-28' 106 ResponseError = ResponseErrorFactory 107 currencycode = 'USD' 108 109 def __init__(self, *args, **kw): 110 self.currencycode = kw.pop('CurrencyCode', self.currencycode) 111 kw.setdefault('host', 'fps.sandbox.amazonaws.com') 112 super(FPSConnection, self).__init__(*args, **kw) 113 114 def _required_auth_capability(self): 115 return ['fps'] 116 117 @needs_caller_reference 118 @complex_amounts('SettlementAmount') 119 @requires(['CreditInstrumentId', 'SettlementAmount.Value', 120 'SenderTokenId', 'SettlementAmount.CurrencyCode']) 121 @api_action() 122 def settle_debt(self, action, response, **kw): 123 """ 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. 127 """ 128 return self.get_object(action, kw, response) 129 130 @requires(['TransactionId']) 131 @api_action() 132 def get_transaction_status(self, action, response, **kw): 133 """ 134 Gets the latest status of a transaction. 135 """ 136 return self.get_object(action, kw, response) 137 138 @requires(['StartDate']) 139 @api_action() 140 def get_account_activity(self, action, response, **kw): 141 """ 142 Returns transactions for a given date range. 143 """ 144 return self.get_object(action, kw, response) 145 146 @requires(['TransactionId']) 147 @api_action() 148 def get_transaction(self, action, response, **kw): 149 """ 150 Returns all details of a transaction. 151 """ 152 return self.get_object(action, kw, response) 153 154 @api_action() 155 def get_outstanding_debt_balance(self, action, response): 156 """ 157 Returns the total outstanding balance for all the credit instruments 158 for the given creditor account. 159 """ 160 return self.get_object(action, {}, response) 161 162 @requires(['PrepaidInstrumentId']) 163 @api_action() 164 def get_prepaid_balance(self, action, response, **kw): 165 """ 166 Returns the balance available on the given prepaid instrument. 167 """ 168 return self.get_object(action, kw, response) 169 170 @api_action() 171 def get_total_prepaid_liability(self, action, response): 172 """ 173 Returns the total liability held by the given account corresponding to 174 all the prepaid instruments owned by the account. 175 """ 176 return self.get_object(action, {}, response) 177 178 @api_action() 179 def get_account_balance(self, action, response): 180 """ 181 Returns the account balance for an account in real time. 182 """ 183 return self.get_object(action, {}, response) 184 185 @needs_caller_reference 186 @requires(['PaymentInstruction', 'TokenType']) 187 @api_action() 188 def install_payment_instruction(self, action, response, **kw): 189 """ 190 Installs a payment instruction for caller. 191 """ 192 return self.get_object(action, kw, response) 193 194 @needs_caller_reference 195 @requires(['returnURL', 'pipelineName']) 196 def cbui_url(self, **kw): 197 """ 198 Generate a signed URL for the Co-Branded service API given arguments as 199 payload. 200 """ 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' 204 205 validpipelines = ('SingleUse', 'MultiUse', 'Recurring', 'Recipient', 206 'SetupPrepaid', 'SetupPostpaid', 'EditToken') 207 assert kw['pipelineName'] in validpipelines, "Invalid pipelineName" 208 kw.update({ 209 'signatureMethod': 'HmacSHA256', 210 'signatureVersion': '2', 211 }) 212 kw.setdefault('callerKey', self.aws_access_key_id) 213 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()]) 217 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))] 222 payload.sort() 223 224 return 'https://{0}{1}?{2}'.format(endpoint, base, encoded(payload)) 225 226 @needs_caller_reference 227 @complex_amounts('TransactionAmount') 228 @requires(['SenderTokenId', 'TransactionAmount.Value', 229 'TransactionAmount.CurrencyCode']) 230 @api_action() 231 def reserve(self, action, response, **kw): 232 """ 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 235 a timing difference. 236 """ 237 return self.get_object(action, kw, response) 238 239 @needs_caller_reference 240 @complex_amounts('TransactionAmount') 241 @requires(['SenderTokenId', 'TransactionAmount.Value', 242 'TransactionAmount.CurrencyCode']) 243 @api_action() 244 def pay(self, action, response, **kw): 245 """ 246 Allows calling applications to move money from a sender to a recipient. 247 """ 248 return self.get_object(action, kw, response) 249 250 @requires(['TransactionId']) 251 @api_action() 252 def cancel(self, action, response, **kw): 253 """ 254 Cancels an ongoing transaction and puts it in cancelled state. 255 """ 256 return self.get_object(action, kw, response) 257 258 @complex_amounts('TransactionAmount') 259 @requires(['ReserveTransactionId', 'TransactionAmount.Value', 260 'TransactionAmount.CurrencyCode']) 261 @api_action() 262 def settle(self, action, response, **kw): 263 """ 264 The Settle API is used in conjunction with the Reserve API and is used 265 to settle previously reserved transaction. 266 """ 267 return self.get_object(action, kw, response) 268 269 @complex_amounts('RefundAmount') 270 @requires(['TransactionId', 'RefundAmount.Value', 271 'CallerReference', 'RefundAmount.CurrencyCode']) 272 @api_action() 273 def refund(self, action, response, **kw): 274 """ 275 Refunds a previously completed transaction. 276 """ 277 return self.get_object(action, kw, response) 278 279 @requires(['RecipientTokenId']) 280 @api_action() 281 def get_recipient_verification_status(self, action, response, **kw): 282 """ 283 Returns the recipient status. 284 """ 285 return self.get_object(action, kw, response) 286 287 @requires(['CallerReference'], ['TokenId']) 288 @api_action() 289 def get_token_by_caller(self, action, response, **kw): 290 """ 291 Returns the details of a particular token installed by this calling 292 application using the subway co-branded UI. 293 """ 294 return self.get_object(action, kw, response) 295 296 @requires(['UrlEndPoint', 'HttpParameters']) 297 @api_action() 298 def verify_signature(self, action, response, **kw): 299 """ 300 Verify the signature that FPS sent in IPN or callback urls. 301 """ 302 return self.get_object(action, kw, response) 303 304 @api_action() 305 def get_tokens(self, action, response, **kw): 306 """ 307 Returns a list of tokens installed on the given account. 308 """ 309 return self.get_object(action, kw, response) 310 311 @requires(['TokenId']) 312 @api_action() 313 def get_token_usage(self, action, response, **kw): 314 """ 315 Returns the usage of a token. 316 """ 317 return self.get_object(action, kw, response) 318 319 @requires(['TokenId']) 320 @api_action() 321 def cancel_token(self, action, response, **kw): 322 """ 323 Cancels any token installed by the calling application on its own 324 account. 325 """ 326 return self.get_object(action, kw, response) 327 328 @needs_caller_reference 329 @complex_amounts('FundingAmount') 330 @requires(['PrepaidInstrumentId', 'FundingAmount.Value', 331 'SenderTokenId', 'FundingAmount.CurrencyCode']) 332 @api_action() 333 def fund_prepaid(self, action, response, **kw): 334 """ 335 Funds the prepaid balance on the given prepaid instrument. 336 """ 337 return self.get_object(action, kw, response) 338 339 @requires(['CreditInstrumentId']) 340 @api_action() 341 def get_debt_balance(self, action, response, **kw): 342 """ 343 Returns the balance corresponding to the given credit instrument. 344 """ 345 return self.get_object(action, kw, response) 346 347 @needs_caller_reference 348 @complex_amounts('AdjustmentAmount') 349 @requires(['CreditInstrumentId', 'AdjustmentAmount.Value', 350 'AdjustmentAmount.CurrencyCode']) 351 @api_action() 352 def write_off_debt(self, action, response, **kw): 353 """ 354 Allows a creditor to write off the debt balance accumulated partially 355 or fully at any time. 356 """ 357 return self.get_object(action, kw, response) 358 359 @requires(['SubscriptionId']) 360 @api_action() 361 def get_transactions_for_subscription(self, action, response, **kw): 362 """ 363 Returns the transactions for a given subscriptionID. 364 """ 365 return self.get_object(action, kw, response) 366 367 @requires(['SubscriptionId']) 368 @api_action() 369 def get_subscription_details(self, action, response, **kw): 370 """ 371 Returns the details of Subscription for a given subscriptionID. 372 """ 373 return self.get_object(action, kw, response) 374 375 @needs_caller_reference 376 @complex_amounts('RefundAmount') 377 @requires(['SubscriptionId']) 378 @api_action() 379 def cancel_subscription_and_refund(self, action, response, **kw): 380 """ 381 Cancels a subscription. 382 """ 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) 388 389 @requires(['TokenId']) 390 @api_action() 391 def get_payment_instruction(self, action, response, **kw): 392 """ 393 Gets the payment instruction of a token. 394 """ 395 return self.get_object(action, kw, response) 396