Skip to content

Commit 6485c31

Browse files
kevinpjonesclaude
andcommitted
BILL-5584: Add descriptor_code support to bank account verification
Stripe updated microdeposit verification from two amounts to a single 6-character descriptor code. This adds support for the new path while keeping the existing amounts path fully backwards compatible. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ef16dbf commit 6485c31

4 files changed

Lines changed: 107 additions & 13 deletions

File tree

lob_python/model/bank_account.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def openapi_types():
124124
'bank_name': (str, type(None)), # noqa: E501
125125
'verified': (bool, type(None)), # noqa: E501
126126
'deleted': (bool, type(None)), # noqa: E501
127+
'microdeposit_type': (str, type(None)), # noqa: E501
127128
}
128129

129130
@cached_property
@@ -146,6 +147,7 @@ def discriminator():
146147
'bank_name': 'bank_name', # noqa: E501
147148
'verified': 'verified', # noqa: E501
148149
'deleted': 'deleted', # noqa: E501
150+
'microdeposit_type': 'microdeposit_type', # noqa: E501
149151
}
150152

151153
read_only_vars = {

lob_python/model/bank_account_verify.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from lob_python.model_utils import ( # noqa: F401
1616
ApiTypeError,
17+
ApiValueError,
1718
ModelComposed,
1819
ModelNormal,
1920
ModelSimple,
@@ -65,6 +66,11 @@ class BankAccountVerify(ModelNormal):
6566
'max_items': 2,
6667
'min_items': 2,
6768
},
69+
('descriptor_code',): {
70+
'regex': {
71+
'pattern': r'^SM[a-zA-Z0-9]{4}$',
72+
},
73+
},
6874
}
6975

7076
@cached_property
@@ -89,6 +95,7 @@ def openapi_types():
8995
"""
9096
return {
9197
'amounts': (list,), # noqa: E501
98+
'descriptor_code': (str,), # noqa: E501
9299
}
93100

94101
@cached_property
@@ -98,6 +105,7 @@ def discriminator():
98105

99106
attribute_map = {
100107
'amounts': 'amounts', # noqa: E501
108+
'descriptor_code': 'descriptor_code', # noqa: E501
101109
}
102110

103111
read_only_vars = {
@@ -107,13 +115,12 @@ def discriminator():
107115

108116
@classmethod
109117
@convert_js_args_to_python_args
110-
def _from_openapi_data(cls, amounts, *args, **kwargs): # noqa: E501
118+
def _from_openapi_data(cls, *args, **kwargs): # noqa: E501
111119
"""BankAccountVerify - a model defined in OpenAPI
112120
113-
Args:
114-
amounts (list): In live mode, an array containing the two micro deposits (in cents) placed in the bank account. In test mode, no micro deposits will be placed, so any two integers between `1` and `100` will work.
115-
116121
Keyword Args:
122+
amounts (list): In live mode, an array containing the two micro deposits (in cents) placed in the bank account. In test mode, no micro deposits will be placed, so any two integers between `1` and `100` will work. [optional] # noqa: E501
123+
descriptor_code (str): The 6-character code (beginning with SM) from the bank statement descriptor of the single $0.01 microdeposit. Required when microdeposit_type is descriptor_code. [optional] # noqa: E501
117124
_check_type (bool): if True, values for parameters in openapi_types
118125
will be type checked and a TypeError will be
119126
raised if the wrong type is input.
@@ -171,7 +178,6 @@ def _from_openapi_data(cls, amounts, *args, **kwargs): # noqa: E501
171178
self._configuration = _configuration
172179
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
173180

174-
self.amounts = amounts
175181
for var_name, var_value in kwargs.items():
176182
if var_name not in self.attribute_map and \
177183
self._configuration is not None and \
@@ -192,13 +198,12 @@ def _from_openapi_data(cls, amounts, *args, **kwargs): # noqa: E501
192198
])
193199

194200
@convert_js_args_to_python_args
195-
def __init__(self, amounts, *args, **kwargs): # noqa: E501
201+
def __init__(self, *args, **kwargs): # noqa: E501
196202
"""BankAccountVerify - a model defined in OpenAPI
197203
198-
Args:
199-
amounts ([Cents]): In live mode, an array containing the two micro deposits (in cents) placed in the bank account. In test mode, no micro deposits will be placed, so any two integers between `1` and `100` will work.
200-
201204
Keyword Args:
205+
amounts ([Cents]): In live mode, an array containing the two micro deposits (in cents) placed in the bank account. In test mode, no micro deposits will be placed, so any two integers between `1` and `100` will work. [optional] # noqa: E501
206+
descriptor_code (str): The 6-character code (beginning with SM) from the bank statement descriptor of the single $0.01 microdeposit. Required when microdeposit_type is descriptor_code. [optional] # noqa: E501
202207
_check_type (bool): if True, values for parameters in openapi_types
203208
will be type checked and a TypeError will be
204209
raised if the wrong type is input.
@@ -254,7 +259,18 @@ def __init__(self, amounts, *args, **kwargs): # noqa: E501
254259
self._configuration = _configuration
255260
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
256261

257-
self.amounts = amounts
262+
has_amounts = 'amounts' in kwargs
263+
has_descriptor_code = 'descriptor_code' in kwargs
264+
265+
if not has_amounts and not has_descriptor_code:
266+
raise ApiValueError(
267+
"one of `amounts` or `descriptor_code` must be provided"
268+
)
269+
if has_amounts and has_descriptor_code:
270+
raise ApiValueError(
271+
"only one of `amounts` or `descriptor_code` may be provided"
272+
)
273+
258274
for var_name, var_value in kwargs.items():
259275
if var_name not in self.attribute_map and \
260276
self._configuration is not None and \

test/Integration/test_bank_accounts_api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,21 @@ def test_delete404(self):
271271
self.api.delete("bank_fake")
272272
self.assertTrue("bank account not found" in context.exception.__str__())
273273

274+
def test_verify_with_descriptor_code200(self):
275+
"""Test case for verify using descriptor_code path"""
276+
bank_verify = BankAccountVerify(descriptor_code="SM11AA")
277+
created_bank = self.api.create(self.bank_writable)
278+
verified_bank_acc = self.api.verify(created_bank.id, bank_verify)
279+
self.bank_ids.append(verified_bank_acc.id)
280+
self.assertIsNotNone(verified_bank_acc.id)
281+
282+
def test_bank_account_has_microdeposit_type(self):
283+
"""Test that a freshly created bank account exposes microdeposit_type"""
284+
created_bank = self.api.create(self.bank_writable)
285+
retrieved_bank = self.api.get(created_bank.id)
286+
self.bank_ids.append(created_bank.id)
287+
self.assertIn(retrieved_bank.microdeposit_type, ["amounts", "descriptor_code", None])
288+
274289

275290
if __name__ == '__main__':
276291
unittest.main()

test/Unit/test_bank_accounts_api.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515

1616
import lob_python
1717
import os
18+
from lob_python.model.bank_account import BankAccount
1819
from lob_python.model.bank_account_verify import BankAccountVerify
1920
from lob_python.model.bank_type_enum import BankTypeEnum
2021
from lob_python.api.bank_accounts_api import BankAccountsApi # noqa: E501
2122
from lob_python.model.bank_account_writable import BankAccountWritable # noqa: E501
2223
from lob_python.model.metadata_model import MetadataModel
2324
from lob_python.model.include_model import IncludeModel
2425
from lob_python.exceptions import UnauthorizedException, NotFoundException, ApiException
26+
from lob_python.model_utils import ApiValueError
2527
from unittest.mock import Mock, MagicMock
2628

2729
class TestBankAccountsApi(unittest.TestCase):
@@ -42,9 +44,7 @@ def setUp(self):
4244
signatory = "fake signatory",
4345
)
4446

45-
self.bank_account_verify = BankAccountVerify(
46-
amounts = [1, 2]
47-
)
47+
self.bank_account_verify = BankAccountVerify(amounts=[1, 2])
4848

4949
self.mock_list_of_bank_accounts = MagicMock(return_value={
5050
"data": [{ "id": "fake 1" }, { "id": "fake 2" }]
@@ -217,5 +217,66 @@ def test_bank_account_delete_error_handle(self):
217217
self.mock_api.bank_account_delete("bank_fakeId")
218218
self.assertTrue("Not Found" in context.exception.__str__())
219219

220+
def test_bank_account_verify_with_descriptor_code(self):
221+
"""Test case for creating BankAccountVerify with descriptor_code"""
222+
verify = BankAccountVerify(descriptor_code="SM11AA")
223+
self.assertIsNotNone(verify)
224+
225+
def test_bank_account_verify_fails_with_both_amounts_and_descriptor_code(self):
226+
"""Test that providing both amounts and descriptor_code raises ApiValueError"""
227+
with self.assertRaises(ApiValueError) as context:
228+
BankAccountVerify(amounts=[1, 2], descriptor_code="SM11AA")
229+
self.assertIn("only one of", str(context.exception))
230+
231+
def test_bank_account_verify_fails_with_neither(self):
232+
"""Test that providing neither amounts nor descriptor_code raises ApiValueError"""
233+
with self.assertRaises(ApiValueError) as context:
234+
BankAccountVerify()
235+
self.assertIn("one of `amounts` or `descriptor_code` must be provided", str(context.exception))
236+
237+
def test_bank_account_verify_fails_with_invalid_descriptor_code_pattern(self):
238+
"""Test that an invalid descriptor_code pattern raises a validation error"""
239+
with self.assertRaises(Exception):
240+
BankAccountVerify(descriptor_code="INVALID")
241+
242+
def test_bank_account_has_microdeposit_type(self):
243+
"""Test that BankAccount accepts and returns microdeposit_type"""
244+
import datetime
245+
account = BankAccount(
246+
routing_number="322271627",
247+
account_number="123456789",
248+
account_type="individual",
249+
signatory="Test User",
250+
id="bank_fakeId",
251+
date_created=datetime.datetime.now(),
252+
date_modified=datetime.datetime.now(),
253+
microdeposit_type="amounts",
254+
)
255+
self.assertEqual(account.microdeposit_type, "amounts")
256+
257+
account2 = BankAccount(
258+
routing_number="322271627",
259+
account_number="123456789",
260+
account_type="individual",
261+
signatory="Test User",
262+
id="bank_fakeId2",
263+
date_created=datetime.datetime.now(),
264+
date_modified=datetime.datetime.now(),
265+
microdeposit_type="descriptor_code",
266+
)
267+
self.assertEqual(account2.microdeposit_type, "descriptor_code")
268+
269+
account3 = BankAccount(
270+
routing_number="322271627",
271+
account_number="123456789",
272+
account_type="individual",
273+
signatory="Test User",
274+
id="bank_fakeId3",
275+
date_created=datetime.datetime.now(),
276+
date_modified=datetime.datetime.now(),
277+
)
278+
self.assertFalse(hasattr(account3, 'microdeposit_type') and account3.microdeposit_type is not None)
279+
280+
220281
if __name__ == '__main__':
221282
unittest.main()

0 commit comments

Comments
 (0)