transaction: Unnamed cart items fall back to the transaction subject.

This commit is contained in:
Brett Smith 2020-11-18 16:56:15 -05:00
parent 23608de591
commit 0bd6353105
2 changed files with 71 additions and 44 deletions

View file

@ -47,7 +47,7 @@ class CartItem(NamedTuple):
total_price: Amount total_price: Amount
@classmethod @classmethod
def from_api(cls, source: APIResponse) -> 'CartItem': def from_api(cls, source: APIResponse, default_name: Optional[str]=None) -> 'CartItem':
total_price = Amount.from_api(source['item_amount']) total_price = Amount.from_api(source['item_amount'])
quantity = Decimal(source.get('item_quantity', 1)) quantity = Decimal(source.get('item_quantity', 1))
try: try:
@ -56,7 +56,7 @@ class CartItem(NamedTuple):
unit_price = total_price._replace(number=total_price.number / quantity) unit_price = total_price._replace(number=total_price.number / quantity)
return cls( return cls(
source.get('item_code'), source.get('item_code'),
source.get('item_name'), source.get('item_name', default_name),
source.get('item_description'), source.get('item_description'),
quantity, quantity,
unit_price, unit_price,
@ -93,41 +93,32 @@ class Transaction(APIResponse):
def __len__(self) -> int: def __len__(self) -> int:
return len(self._response) return len(self._response)
def _from_response( # type:ignore[misc] def _get_from_response(self, *keys: str) -> Any:
func: Callable[[Any], T], retval = self._response
*keys: str,
) -> Callable[['Transaction'], T]:
def _load_from_response(self: 'Transaction') -> T:
source = self._response
for index, key in enumerate(keys): for index, key in enumerate(keys):
try: try:
source = source[key] retval = retval[key]
except KeyError as error: except KeyError as error:
try: try:
txn_id = f"Transaction {self['transaction_info']['transaction_id']}" txn_id = f"Transaction {self['transaction_info']['transaction_id']}"
except KeyError: except KeyError:
txn_id = "Transaction" txn_id = "Transaction"
if source is self._response: if index:
key_s = ''.join(repr(key) for key in keys[:index + 1])
raise KeyError(f"{txn_id} {key_s}") from None
else:
raise errors.MissingFieldError( raise errors.MissingFieldError(
f"{txn_id} was not loaded with {error.args[0]!r} field", f"{txn_id} was not loaded with {error.args[0]!r} field",
) from None ) from None
else: return retval
key_s = ''.join(repr(key) for key in keys[:index + 1])
raise KeyError(f"{txn_id} {key_s}") from None
return func(source)
return _load_from_response
def _cart_items(cart_info: APIResponse) -> Iterator[CartItem]: # type:ignore[misc] def _wrap_response( # type:ignore[misc]
try: func: Callable[[Any], T],
item_seq = cart_info['item_details'] *keys: str,
except KeyError: ) -> Callable[['Transaction'], T]:
pass def response_wrapper(self: 'Transaction') -> T:
else: return func(self._get_from_response(*keys))
for source in item_seq: return response_wrapper
try:
yield CartItem.from_api(source)
except KeyError:
pass
def _fee_amount(txn_info: APIResponse) -> Optional[Amount]: # type:ignore[misc] def _fee_amount(txn_info: APIResponse) -> Optional[Amount]: # type:ignore[misc]
try: try:
@ -137,28 +128,44 @@ class Transaction(APIResponse):
else: else:
return Amount.from_api(raw_fee) return Amount.from_api(raw_fee)
amount = _from_response( amount = _wrap_response(
Amount.from_api, Amount.from_api,
'transaction_info', 'transaction_info',
'transaction_amount', 'transaction_amount',
) )
cart_items = _from_response(_cart_items, 'cart_info') fee_amount = _wrap_response(_fee_amount, 'transaction_info')
fee_amount = _from_response(_fee_amount, 'transaction_info') initiation_date = _wrap_response(
initiation_date = _from_response(
parse_datetime, parse_datetime,
'transaction_info', 'transaction_info',
'transaction_initiation_date', 'transaction_initiation_date',
) )
payer_email = _from_response(str, 'payer_info', 'email_address') payer_email = _wrap_response(str, 'payer_info', 'email_address')
payer_fullname = _from_response(str, 'payer_info', 'payer_name', 'alternate_full_name') payer_fullname = _wrap_response(str, 'payer_info', 'payer_name', 'alternate_full_name')
status = _from_response( status = _wrap_response(
TransactionStatus.__getitem__, TransactionStatus.__getitem__,
'transaction_info', 'transaction_info',
'transaction_status', 'transaction_status',
) )
transaction_id = _from_response(str, 'transaction_info', 'transaction_id') transaction_id = _wrap_response(str, 'transaction_info', 'transaction_id')
updated_date = _from_response( updated_date = _wrap_response(
parse_datetime, parse_datetime,
'transaction_info', 'transaction_info',
'transaction_updated_date', 'transaction_updated_date',
) )
def cart_items(self) -> Iterator[CartItem]:
cart_info = self._get_from_response('cart_info')
try:
item_seq = cart_info['item_details']
except KeyError:
pass
else:
try:
default_name = self['transaction_info']['transaction_subject']
except KeyError:
default_name = None
for source in item_seq:
try:
yield CartItem.from_api(source, default_name)
except KeyError:
pass

View file

@ -144,6 +144,20 @@ def test_cart_items():
(Decimal('15.98'), 'USD'), (Decimal('15.98'), 'USD'),
) )
@pytest.mark.parametrize('subject', [
'test subject',
'$6.99 Subscription',
None,
])
def test_unnamed_cart_item_uses_transaction_subject(subject):
source = {
'cart_info': cart_info({'number': '6.99'}),
'transaction_info': transaction_info(subject=subject),
}
txn = txn_mod.Transaction(source)
item1, = txn.cart_items()
assert item1.name == subject
def test_cart_empty(): def test_cart_empty():
txn = txn_mod.Transaction({'cart_info': {}}) txn = txn_mod.Transaction({'cart_info': {}})
assert not any(txn.cart_items()) assert not any(txn.cart_items())
@ -217,4 +231,10 @@ def test_updated_date():
def test_info_missing(method_name): def test_info_missing(method_name):
txn = txn_mod.Transaction({}) txn = txn_mod.Transaction({})
with pytest.raises(errors.MissingFieldError): with pytest.raises(errors.MissingFieldError):
getattr(txn, method_name)() result = getattr(txn, method_name)()
try:
do_next = iter(result) is result
except TypeError:
do_next = False
if do_next:
next(result)