diff --git a/tests/test_api_client/test_deserializer.py b/tests/test_api_client/test_deserializer.py index 9c851a8a..3bba4fcc 100644 --- a/tests/test_api_client/test_deserializer.py +++ b/tests/test_api_client/test_deserializer.py @@ -381,3 +381,33 @@ def test_deserialize_model_enum(data, expected): # then correct Shape enum expected assert isinstance(result, Shape) assert result == expected + + +def test_deserialize_contact_strips_tax_number_type_namespace(): + from xero_python.accounting.models.contact import Contact + + contact = deserialize_model( + Contact, {"TaxNumberType": "TAXNUMBERTYPE/SSN"}, model_finder=None + ) + + assert contact.tax_number_type == "SSN" + + +def test_deserialize_contact_keeps_valid_tax_number_type(): + from xero_python.accounting.models.contact import Contact + + contact = deserialize_model(Contact, {"TaxNumberType": "EIN"}, model_finder=None) + + assert contact.tax_number_type == "EIN" + + +def test_deserialize_linked_transaction_accepts_receipt_source(): + from xero_python.accounting.models.linked_transaction import LinkedTransaction + + txn = deserialize_model( + LinkedTransaction, + {"SourceTransactionTypeCode": "RECEIPT"}, + model_finder=None, + ) + + assert txn.source_transaction_type_code == "RECEIPT" diff --git a/xero_python/api_client/deserializer.py b/xero_python/api_client/deserializer.py index b64e9820..e40acb7f 100644 --- a/xero_python/api_client/deserializer.py +++ b/xero_python/api_client/deserializer.py @@ -284,5 +284,37 @@ def deserialize_model(model, data, model_finder): value = data[attr_key] kwargs[attr] = deserialize(attr_type, value, model_finder) - instance = model(**kwargs) + try: + return model(**kwargs) + except ValueError: + # Generated setters reject enum members the live API still returns + # (xero-python#203, #205, #206). Keep valid data; do not fail the + # whole payload because one closed enum is behind the spec. + return _deserialize_model_preserving_api_enums(model, kwargs) + + +def _enum_namespace_suffix(value): + if isinstance(value, str) and "/" in value: + return value.rsplit("/", 1)[-1] + return value + + +def _deserialize_model_preserving_api_enums(model, kwargs): + instance = model() + for attr, value in kwargs.items(): + try: + setattr(instance, attr, value) + continue + except ValueError as err: + stripped = _enum_namespace_suffix(value) + if stripped != value: + try: + setattr(instance, attr, stripped) + continue + except ValueError: + pass + private = "_{}".format(attr) + if not hasattr(instance, private): + raise err + setattr(instance, private, value) return instance