Home | History | Annotate | Download | only in fps
      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 
     24 import urllib
     25 import uuid
     26 from boto.connection import AWSQueryConnection
     27 from boto.fps.exception import ResponseErrorFactory
     28 from boto.fps.response import ResponseFactory
     29 import boto.fps.response
     30 
     31 __all__ = ['FPSConnection']
     32 
     33 decorated_attrs = ('action', 'response')
     34 
     35 
     36 def add_attrs_from(func, to):
     37     for attr in decorated_attrs:
     38         setattr(to, attr, getattr(func, attr, None))
     39     return to
     40 
     41 
     42 def 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 
     57 def 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 
     76 def 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 
     86 def 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 
    103 class 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