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)}))