From b10bd56427d043114f56de79cf95937e02d61ef3 Mon Sep 17 00:00:00 2001 From: Kayvan Zahiri <123409429+Kayvan-Zahiri@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:26:17 -0700 Subject: [PATCH] Preserve instants when serializing timezone-aware XML datetimes --- CHANGELOG.md | 3 +++ braintree/util/generator.py | 2 ++ tests/unit/test_generator_timezones.py | 29 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 tests/unit/test_generator_timezones.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 546e996c..047287ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Unreleased +* Convert timezone-aware datetimes to UTC before serializing them with a `Z` suffix + ## 4.48.0 * Add `Transaction.reverse()` to automatically void or fully refund a transaction * Add `previous_mastercard_transaction_link_id` to `external_vault` on `Transaction` and `CreditCardVerification.create` diff --git a/braintree/util/generator.py b/braintree/util/generator.py index b7b68eca..5ed75fc9 100644 --- a/braintree/util/generator.py +++ b/braintree/util/generator.py @@ -20,6 +20,8 @@ def __generate_boolean(self, value): return str(value).lower() def __generate_datetime(self, value): + if isinstance(value, datetime.datetime) and value.utcoffset() is not None: + value = value.astimezone(datetime.timezone.utc) return value.strftime("%Y-%m-%dT%H:%M:%SZ") def __generate_dict(self, dictionary): diff --git a/tests/unit/test_generator_timezones.py b/tests/unit/test_generator_timezones.py new file mode 100644 index 00000000..2d860d43 --- /dev/null +++ b/tests/unit/test_generator_timezones.py @@ -0,0 +1,29 @@ +import unittest +from datetime import date, datetime, timedelta, timezone + +from braintree.util.xml_util import XmlUtil + + +class TestGeneratorTimezones(unittest.TestCase): + def test_positive_offset_preserves_instant_across_date_boundary(self): + value = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone(timedelta(hours=5, minutes=30))) + xml = XmlUtil.xml_from_dict({"created_at": value}) + self.assertEqual('2026-01-01T21:34:05Z', xml) + self.assertEqual({"created_at": datetime(2026, 1, 1, 21, 34, 5)}, XmlUtil.dict_from_xml(xml)) + + def test_negative_offset_preserves_instant(self): + value = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone(timedelta(hours=-7))) + xml = XmlUtil.xml_from_dict({"created_at": value}) + self.assertEqual('2026-01-02T10:04:05Z', xml) + self.assertEqual({"created_at": datetime(2026, 1, 2, 10, 4, 5)}, XmlUtil.dict_from_xml(xml)) + + def test_utc_keeps_existing_output(self): + value = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + self.assertEqual('2026-01-02T03:04:05Z', XmlUtil.xml_from_dict({"value": value})) + + def test_naive_datetime_keeps_existing_output(self): + value = datetime(2026, 1, 2, 3, 4, 5) + self.assertEqual('2026-01-02T03:04:05Z', XmlUtil.xml_from_dict({"value": value})) + + def test_date_keeps_existing_output(self): + self.assertEqual('2026-01-02T00:00:00Z', XmlUtil.xml_from_dict({"value": date(2026, 1, 2)}))