Skip to content

Commit

Permalink
Feat: endpoint to get funding source linked groups (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
thekaveman authored Jun 4, 2024
2 parents 9191b43 + 27aedeb commit 54e2f2f
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 46 deletions.
55 changes: 54 additions & 1 deletion littlepay/api/funding_sources.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
from typing import Generator, List, Optional

from littlepay.api import ClientProtocol

Expand All @@ -25,6 +26,46 @@ class FundingSourceResponse:
icc_hash: Optional[str] = None


@dataclass
class FundingSourceDateFields:
"""Implements parsing of datetime strings to Python datetime objects for funding source fields."""

created_date: datetime | None = None
updated_date: datetime | None = None
expiry_date: datetime | None = None

def __post_init__(self):
"""Parses any date parameters into Python datetime objects.
For @dataclasses with a generated __init__ function, this function is called automatically.
Includes a workaround for Python 3.10 where datetime.fromisoformat() can only parse the format output
by datetime.isoformat(), i.e. without a trailing 'Z' offset character and with UTC offset expressed
as +/-HH:mm
https://docs.python.org/3.11/library/datetime.html#datetime.datetime.fromisoformat
"""
if self.created_date:
self.created_date = datetime.fromisoformat(self.created_date.replace("Z", "+00:00", 1))
else:
self.created_date = None
if self.updated_date:
self.updated_date = datetime.fromisoformat(self.updated_date.replace("Z", "+00:00", 1))
else:
self.updated_date = None
if self.expiry_date:
self.expiry_date = datetime.fromisoformat(self.expiry_date.replace("Z", "+00:00", 1))
else:
self.expiry_date = None


@dataclass(kw_only=True)
class FundingSourceGroupResponse(FundingSourceDateFields):
id: str
group_id: str
label: str


class FundingSourcesMixin(ClientProtocol):
"""Mixin implements APIs for funding sources."""

Expand All @@ -34,7 +75,19 @@ def funding_source_by_token_endpoint(self, card_token) -> str:
"""Endpoint for a funding source by card token."""
return self._make_endpoint(self.FUNDING_SOURCES, "bytoken", card_token)

def funding_source_concession_groups_endpoint(self, funding_source_id) -> str:
"""Endpoint for a funding source's concession groups."""
return self._make_endpoint(self.FUNDING_SOURCES, funding_source_id, "concession_groups")

def get_funding_source_by_token(self, card_token) -> FundingSourceResponse:
"""Return a FundingSourceResponse object from the funding source by token endpoint."""
endpoint = self.funding_source_by_token_endpoint(card_token)
return self._get(endpoint, FundingSourceResponse)

def get_funding_source_linked_concession_groups(
self, funding_source_id: str
) -> Generator[FundingSourceGroupResponse, None, None]:
"""Yield FundingSourceGroupResponse objects representing linked concession groups."""
endpoint = self.funding_source_concession_groups_endpoint(funding_source_id)
for item in self._get_list(endpoint, per_page=100):
yield FundingSourceGroupResponse(**item)
31 changes: 3 additions & 28 deletions littlepay/api/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Generator

from littlepay.api import ClientProtocol, ListResponse
from littlepay.api.funding_sources import FundingSourcesMixin
from littlepay.api.funding_sources import FundingSourceDateFields, FundingSourcesMixin


@dataclass
Expand All @@ -25,34 +25,9 @@ def csv_header() -> str:
return ",".join(vars(instance).keys())


@dataclass
class GroupFundingSourceResponse:
@dataclass(kw_only=True)
class GroupFundingSourceResponse(FundingSourceDateFields):
id: str
created_date: datetime | None = None
updated_date: datetime | None = None
expiry_date: datetime | None = None

def __post_init__(self):
"""Parses any date parameters into Python datetime objects.
Includes a workaround for Python 3.10 where datetime.fromisoformat() can only parse the format output
by datetime.isoformat(), i.e. without a trailing 'Z' offset character and with UTC offset expressed
as +/-HH:mm
https://docs.python.org/3.11/library/datetime.html#datetime.datetime.fromisoformat
"""
if self.created_date:
self.created_date = datetime.fromisoformat(self.created_date.replace("Z", "+00:00", 1))
else:
self.created_date = None
if self.updated_date:
self.updated_date = datetime.fromisoformat(self.updated_date.replace("Z", "+00:00", 1))
else:
self.updated_date = None
if self.expiry_date:
self.expiry_date = datetime.fromisoformat(self.expiry_date.replace("Z", "+00:00", 1))
else:
self.expiry_date = None


class GroupsMixin(ClientProtocol):
Expand Down
126 changes: 125 additions & 1 deletion tests/api/test_funding_sources.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
from typing import Generator
import pytest

from littlepay.api.funding_sources import FundingSourceResponse, FundingSourcesMixin
from littlepay.api import ListResponse
from littlepay.api.funding_sources import (
FundingSourceDateFields,
FundingSourceGroupResponse,
FundingSourceResponse,
FundingSourcesMixin,
)


@pytest.fixture
def ListResponse_FundingSourceGroups(expected_expiry_str):
items = [
dict(
id="0",
group_id="zero",
label="label0",
expiry_date=expected_expiry_str,
created_date=expected_expiry_str,
updated_date=expected_expiry_str,
),
dict(
id="1",
group_id="one",
label="label1",
expiry_date=expected_expiry_str,
created_date=expected_expiry_str,
updated_date=expected_expiry_str,
),
dict(id="2", group_id="two", label="label2", expiry_date="", created_date=""),
]
return ListResponse(list=items, total_count=3)


@pytest.fixture
Expand All @@ -20,12 +51,65 @@ def mock_ClientProtocol_get_FundingResource(mocker):
return mocker.patch("littlepay.api.ClientProtocol._get", return_value=funding_source)


@pytest.fixture
def mock_ClientProtocol_get_list_FundingSourceGroup(mocker, ListResponse_FundingSourceGroups):
return mocker.patch(
"littlepay.api.ClientProtocol._get_list",
side_effect=lambda *args, **kwargs: (g for g in ListResponse_FundingSourceGroups.list),
)


def test_FundingSourceDateFields(expected_expiry_str, expected_expiry):
fields = FundingSourceDateFields(
created_date=expected_expiry_str, updated_date=expected_expiry_str, expiry_date=expected_expiry_str
)

assert fields.created_date == expected_expiry
assert fields.updated_date == expected_expiry
assert fields.expiry_date == expected_expiry


def test_FundingSourceGroupResponse_no_dates():
response = FundingSourceGroupResponse(id="id", group_id="group_id", label="label")

assert response.id == "id"
assert response.group_id == "group_id"
assert response.label == "label"
assert response.created_date is None
assert response.updated_date is None
assert response.expiry_date is None


def test_FundingSourceGroupResponse_with_dates(expected_expiry_str, expected_expiry):
response = FundingSourceGroupResponse(
id="id",
group_id="group_d",
label="label",
created_date=expected_expiry_str,
updated_date=expected_expiry_str,
expiry_date=expected_expiry_str,
)

assert response.created_date == expected_expiry
assert response.updated_date == expected_expiry
assert response.expiry_date == expected_expiry


def test_FundingSourcesMixin_funding_sources_by_token_endpoint(url):
client = FundingSourcesMixin()

assert client.funding_source_by_token_endpoint("abc_token") == f"{url}/fundingsources/bytoken/abc_token"


def test_FundingSourcesMixin_funding_source_concession_groups_endpoint(url):
client = FundingSourcesMixin()

assert (
client.funding_source_concession_groups_endpoint("abd_funding_source")
== f"{url}/fundingsources/abd_funding_source/concession_groups"
)


def test_FundingSourcesMixin_get_funding_source_by_token(mock_ClientProtocol_get_FundingResource):
client = FundingSourcesMixin()

Expand All @@ -36,3 +120,43 @@ def test_FundingSourcesMixin_get_funding_source_by_token(mock_ClientProtocol_get
client.funding_source_by_token_endpoint(card_token), FundingSourceResponse
)
assert isinstance(result, FundingSourceResponse)


def test_FundingSourcesMixin_get_concession_group_linked_funding_sources(
ListResponse_FundingSourceGroups, mock_ClientProtocol_get_list_FundingSourceGroup, expected_expiry, expected_expiry_str
):
client = FundingSourcesMixin()

result = client.get_funding_source_linked_concession_groups("funding-source-1234")
assert isinstance(result, Generator)
assert mock_ClientProtocol_get_list_FundingSourceGroup.call_count == 0

result_list = list(result)
mock_ClientProtocol_get_list_FundingSourceGroup.assert_called_once_with(
client.funding_source_concession_groups_endpoint("funding-source-1234"), per_page=100
)

expected_list = ListResponse_FundingSourceGroups.list

assert len(result_list) == len(expected_list)
assert all([isinstance(i, FundingSourceGroupResponse) for i in result_list])

for i in range(len(result_list)):
assert result_list[i].id == expected_list[i]["id"]
assert result_list[i].group_id == expected_list[i]["group_id"]
assert result_list[i].label == expected_list[i]["label"]

if expected_list[i].get("expiry_date") == expected_expiry_str:
assert result_list[i].expiry_date == expected_expiry
else:
assert result_list[i].expiry_date is None

if expected_list[i].get("created_date") == expected_expiry_str:
assert result_list[i].created_date == expected_expiry
else:
assert result_list[i].created_date is None

if expected_list[i].get("updated_date") == expected_expiry_str:
assert result_list[i].updated_date == expected_expiry
else:
assert result_list[i].updated_date is None
18 changes: 5 additions & 13 deletions tests/api/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,6 @@
from littlepay.api.groups import GroupFundingSourceResponse, GroupResponse, GroupsMixin


@pytest.fixture
def expected_expiry():
return datetime(2024, 3, 19, 22, 0, 0, tzinfo=timezone.utc)


@pytest.fixture
def expected_expiry_str(expected_expiry):
return expected_expiry.strftime("%Y-%m-%dT%H:%M:%SZ")


@pytest.fixture
def ListResponse_GroupFundingSources(expected_expiry_str):
items = [
Expand Down Expand Up @@ -93,7 +83,7 @@ def test_GroupResponse_csv_header():


def test_GroupFundingSourceResponse_no_dates():
response = GroupFundingSourceResponse("id")
response = GroupFundingSourceResponse(id="id")

assert response.id == "id"
assert response.expiry_date is None
Expand All @@ -102,7 +92,7 @@ def test_GroupFundingSourceResponse_no_dates():


def test_GroupFundingSourceResponse_empty_dates():
response = GroupFundingSourceResponse("id", "", "", "")
response = GroupFundingSourceResponse(id="id", created_date="", updated_date="", expiry_date="")

assert response.id == "id"
assert response.expiry_date is None
Expand All @@ -111,7 +101,9 @@ def test_GroupFundingSourceResponse_empty_dates():


def test_GroupFundingSourceResponse_with_dates(expected_expiry, expected_expiry_str):
response = GroupFundingSourceResponse("id", expected_expiry_str, expected_expiry_str, expected_expiry_str)
response = GroupFundingSourceResponse(
id="id", created_date=expected_expiry_str, updated_date=expected_expiry_str, expiry_date=expected_expiry_str
)

assert response.id == "id"
assert response.expiry_date == expected_expiry
Expand Down
21 changes: 18 additions & 3 deletions tests/commands/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,24 @@
]

GROUP_FUND_RESPONSES = [
GroupFundingSourceResponse("group_funding_id0", "2024-04-01T00:05:23Z", "2024-04-02T00:05:23Z", "2024-04-03T00:05:23Z"),
GroupFundingSourceResponse("group_funding_id1", "2024-04-04T00:05:23Z", "2024-04-05T00:05:23Z", "2024-04-06T00:05:23Z"),
GroupFundingSourceResponse("group_funding_id2", "2024-04-07T00:05:23Z", "2024-04-08T00:05:23Z", "2024-04-09T00:05:23Z"),
GroupFundingSourceResponse(
id="group_funding_id0",
created_date="2024-04-01T00:05:23Z",
updated_date="2024-04-02T00:05:23Z",
expiry_date="2024-04-03T00:05:23Z",
),
GroupFundingSourceResponse(
id="group_funding_id1",
created_date="2024-04-04T00:05:23Z",
updated_date="2024-04-05T00:05:23Z",
expiry_date="2024-04-06T00:05:23Z",
),
GroupFundingSourceResponse(
id="group_funding_id2",
created_date="2024-04-07T00:05:23Z",
updated_date="2024-04-08T00:05:23Z",
expiry_date="2024-04-09T00:05:23Z",
),
]


Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from pathlib import Path

import pytest
Expand Down Expand Up @@ -146,3 +147,13 @@ def mock_ClientProtocol_make_endpoint(mocker, url):
@pytest.fixture
def ListResponse_sample():
return ListResponse(list=[{"one": 1}, {"two": 2}, {"three": 3}], total_count=3)


@pytest.fixture
def expected_expiry():
return datetime(2024, 3, 19, 22, 0, 0, tzinfo=timezone.utc)


@pytest.fixture
def expected_expiry_str(expected_expiry):
return expected_expiry.strftime("%Y-%m-%dT%H:%M:%SZ")

0 comments on commit 54e2f2f

Please sign in to comment.