Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 585928c

Browse files
authored
34467 Colin API - snapshot, normalize shares and currency (#4752)
* 34467 Colin API - snapshot, normalize shares and currency Signed-off-by: Kial Jinnah <[email protected]> * chore: lint Signed-off-by: Kial Jinnah <[email protected]> --------- Signed-off-by: Kial Jinnah <[email protected]>
1 parent c713e85 commit 585928c

3 files changed

Lines changed: 108 additions & 9 deletions

File tree

colin-api/src/colin_api/models/business_snapshot.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from __future__ import annotations
1616

1717
from datetime import datetime, timezone
18-
from typing import Dict, Optional
18+
from typing import Dict, Optional, Tuple
1919

2020
import pycountry
2121
from flask import current_app
@@ -34,6 +34,9 @@ class BusinessSnapshot: # pylint: disable=too-few-public-methods
3434
# snapshot offices are limited to the two the amalgamation flow prepopulates
3535
OFFICE_TYPES = ('registeredOffice', 'recordsOffice')
3636

37+
# the share name suffix legal-api requires (common_validations.py)
38+
SHARE_NAME_SUFFIX = ' Shares'
39+
3740
@classmethod
3841
def get_snapshot(cls, orig_identifier: str) -> Dict:
3942
"""Return the business/parties/offices/shareClasses/resolutions snapshot."""
@@ -153,17 +156,19 @@ def _normalize_address(cls, address: Optional[Dict]) -> Optional[Dict]:
153156
@classmethod
154157
def _normalize_share_class(cls, share_class: Dict) -> Dict:
155158
"""Return a share class in the structure of LEAR's /share-classes items."""
159+
currency, currency_additional = cls._normalize_currency(
160+
share_class['currency'], share_class['currencyAdditional'])
156161
return {
157162
'id': share_class['id'],
158-
'name': share_class['name'],
163+
'name': cls._normalize_share_name(share_class['name']),
159164
# COLIN never stores a priority - the class id preserves creation order
160165
'priority': share_class['displayOrder'],
161166
'hasMaximumShares': share_class['hasMaximumShares'],
162167
'maxNumberOfShares': cls._to_int(share_class['maxNumberOfShares']),
163168
'hasParValue': share_class['hasParValue'],
164169
'parValue': float(share_class['parValue']) if share_class['parValue'] is not None else None,
165-
'currency': share_class['currency'],
166-
'currencyAdditional': share_class['currencyAdditional'],
170+
'currency': currency,
171+
'currencyAdditional': currency_additional,
167172
'hasRightsOrRestrictions': share_class['hasRightsOrRestrictions'],
168173
'series': [cls._normalize_share_series(series) for series in share_class['series']],
169174
}
@@ -173,13 +178,37 @@ def _normalize_share_series(cls, series: Dict) -> Dict:
173178
"""Return a share series in the structure of LEAR's series json."""
174179
return {
175180
'id': series['id'],
176-
'name': series['name'],
181+
'name': cls._normalize_share_name(series['name']),
177182
'priority': series['displayOrder'],
178183
'hasMaximumShares': series['hasMaximumShares'],
179184
'maxNumberOfShares': cls._to_int(series['maxNumberOfShares']),
180185
'hasRightsOrRestrictions': series['hasRightsOrRestrictions'],
181186
}
182187

188+
@classmethod
189+
def _normalize_share_name(cls, name: Optional[str]) -> Optional[str]:
190+
"""Append the ' Shares' suffix legal-api requires, dropping any existing any-case suffix."""
191+
if not name:
192+
return name
193+
if name.endswith(cls.SHARE_NAME_SUFFIX):
194+
return name
195+
stripped = name.rstrip()
196+
if stripped.lower().endswith(cls.SHARE_NAME_SUFFIX.lower()):
197+
stripped = stripped[:-len(cls.SHARE_NAME_SUFFIX)].rstrip()
198+
return f'{stripped}{cls.SHARE_NAME_SUFFIX}'
199+
200+
@classmethod
201+
def _normalize_currency(cls,
202+
currency: Optional[str],
203+
currency_additional: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
204+
"""Map COLIN's OTH currency the way the tombstone migration does."""
205+
if currency != 'OTH':
206+
return currency, currency_additional
207+
code = (currency_additional or '').strip().upper()
208+
if code and pycountry.currencies.get(alpha_3=code):
209+
return code, None
210+
return 'OTHER', currency_additional
211+
183212
_country_cache: Dict[str, str] = {}
184213

185214
@classmethod

colin-api/src/colin_api/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
Development release segment: .devN
2323
"""
2424

25-
__version__ = '2.172.2' # pylint: disable=invalid-name
25+
__version__ = '2.172.3' # pylint: disable=invalid-name

colin-api/tests/unit/api/test_business_snapshot.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,44 @@
1515
"""Tests to assure the business snapshot end-point."""
1616
from datetime import datetime
1717

18+
import pytest
19+
1820
from colin_api.exceptions import BusinessNotFoundException, PartiesNotFoundException
1921
from colin_api.models import Business, Party, ShareObject
22+
from colin_api.models.shares import Share, ShareClass
2023
from tests.unit import LEAR_ADDRESS, build_business, build_director, bypass_auth
2124

2225

2326
SNAPSHOT_URL = '/api/v1/businesses/BC0870226/snapshot'
2427

2528

29+
def build_share_structure_variant(class_name, currency='CAD', other_currency=None, series_name=None):
30+
"""Return a current ShareObject with the subject class name / currency."""
31+
share_class = ShareClass()
32+
share_class.share_id = 0
33+
share_class.share_name = class_name
34+
share_class.currency_type = currency
35+
share_class.other_currency = other_currency
36+
share_class.has_max_shares = 'N' # COLIN semantics: 'N' means has a maximum
37+
share_class.has_par_value = 'Y'
38+
share_class.has_special_rights = 'Y'
39+
share_class.par_value_amt = 1.5
40+
share_class.max_number_shares = 10000
41+
share_class.series = []
42+
if series_name:
43+
series = Share()
44+
series.share_id = 1
45+
series.share_name = series_name
46+
series.has_max_shares = 'Y'
47+
series.has_special_rights = 'N'
48+
series.max_number_shares = None
49+
share_class.series = [series]
50+
share_struct = ShareObject()
51+
share_struct.end_event_id = None
52+
share_struct.share_classes = [share_class]
53+
return share_struct
54+
55+
2656
def test_get_snapshot(client, mocker, authorized, mock_db, mock_lookups): # pylint: disable=unused-argument
2757
"""Assert the full LEAR-normalized snapshot is returned."""
2858
rv = client.get(SNAPSHOT_URL)
@@ -64,18 +94,18 @@ def test_get_snapshot(client, mocker, authorized, mock_db, mock_lookups): # pyl
6494
},
6595
'shareClasses': [{
6696
'id': 0,
67-
'name': 'CLASS A',
97+
'name': 'CLASS A Shares',
6898
'priority': 0,
6999
'hasMaximumShares': True,
70100
'maxNumberOfShares': 10000,
71101
'hasParValue': True,
72102
'parValue': 1.5,
73-
'currency': 'OTH',
103+
'currency': 'OTHER',
74104
'currencyAdditional': 'BITCOIN',
75105
'hasRightsOrRestrictions': True,
76106
'series': [{
77107
'id': 1,
78-
'name': 'SERIES 1',
108+
'name': 'SERIES 1 Shares',
79109
'priority': 1,
80110
'hasMaximumShares': False,
81111
'maxNumberOfShares': None,
@@ -152,6 +182,46 @@ def test_get_snapshot_without_share_structure(client, mocker, authorized, mock_d
152182
assert rv.json['shareClasses'] == []
153183

154184

185+
@pytest.mark.parametrize('colin_name, expected_name', [
186+
('CLASS A COMMON SHARES', 'CLASS A COMMON Shares'), # all-caps legacy suffix replaced
187+
('preferred shares', 'preferred Shares'), # lowercase suffix replaced (tombstone migration parity)
188+
('Class A Shares', 'Class A Shares'), # already normalized - unchanged
189+
('CLASS B', 'CLASS B Shares'), # no suffix - appended
190+
])
191+
def test_get_snapshot_normalizes_share_names(client, mocker, authorized, mock_db, mock_lookups,
192+
colin_name, expected_name): # pylint: disable=unused-argument
193+
"""Assert class and series names get the ' Shares' suffix legal-api requires."""
194+
mocker.patch.object(ShareObject, 'get_all', return_value=[
195+
build_share_structure_variant(colin_name, series_name='SERIES 1')])
196+
197+
rv = client.get(SNAPSHOT_URL)
198+
199+
assert rv.status_code == 200
200+
assert rv.json['shareClasses'][0]['name'] == expected_name
201+
assert rv.json['shareClasses'][0]['series'][0]['name'] == 'SERIES 1 Shares'
202+
203+
204+
@pytest.mark.parametrize('currency, other_currency, expected, expected_additional', [
205+
('CAD', None, 'CAD', None), # real code passes through
206+
('OTH', 'CAD', 'CAD', None), # tombstone migration parity fold
207+
('OTH', ' usd ', 'USD', None), # any valid ISO 4217 code is promoted
208+
('OTH', 'BITCOIN', 'OTHER', 'BITCOIN'), # non-ISO free text stays flagged
209+
('OTH', None, 'OTHER', None),
210+
])
211+
def test_get_snapshot_normalizes_currency(client, mocker, authorized, mock_db, mock_lookups,
212+
currency, other_currency, expected,
213+
expected_additional): # pylint: disable=unused-argument
214+
"""Assert COLIN's OTH currency is folded to an ISO code or OTHER."""
215+
mocker.patch.object(ShareObject, 'get_all', return_value=[
216+
build_share_structure_variant('Class A Shares', currency=currency, other_currency=other_currency)])
217+
218+
rv = client.get(SNAPSHOT_URL)
219+
220+
assert rv.status_code == 200
221+
assert rv.json['shareClasses'][0]['currency'] == expected
222+
assert rv.json['shareClasses'][0]['currencyAdditional'] == expected_additional
223+
224+
155225
def test_get_snapshot_null_good_standing(client, mocker, authorized, mock_db,
156226
mock_lookups): # pylint: disable=unused-argument
157227
"""Assert COLIN's tri-state good standing passes through as null when unknown."""

0 commit comments

Comments
 (0)