Skip to content

Commit 7ca29a2

Browse files
committed
Feature: aleph credits history
1 parent 9f4c7cc commit 7ca29a2

File tree

2 files changed

+222
-6
lines changed

2 files changed

+222
-6
lines changed

src/aleph_client/commands/credit.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
from typing import Annotated, Optional
44

55
import typer
6+
from aiohttp import ClientResponseError
67
from aleph.sdk import AlephHttpClient
78
from aleph.sdk.account import _load_account
89
from aleph.sdk.conf import settings
910
from aleph.sdk.types import AccountFromPrivateKey
1011
from aleph.sdk.utils import displayable_amount
12+
from rich import box
1113
from rich.console import Console
1214
from rich.panel import Panel
15+
from rich.table import Table
1316
from rich.text import Text
1417

1518
from aleph_client.commands import help_strings
@@ -50,18 +53,101 @@ async def show(
5053
typer.echo(credit.model_dump_json(indent=4))
5154
else:
5255
infos = [
53-
Text.from_markup(f"Address: [bright_cyan]{address}[/bright_cyan]\n"),
56+
Text.from_markup(f"Address: {address}\n"),
5457
Text("Credits:"),
55-
Text.from_markup(f"[bright_cyan] {displayable_amount(credit.credits, decimals=2)}[/bright_cyan]"),
58+
Text.from_markup(f" {displayable_amount(credit.credits, decimals=2)}"),
5659
]
5760
console.print(
5861
Panel(
5962
Text.assemble(*infos),
6063
title="Credits Infos",
61-
border_style="bright_cyan",
64+
border_style="blue",
6265
expand=False,
6366
title_align="left",
6467
)
6568
)
6669
else:
6770
typer.echo("Error: Please provide either a private key, private key file, or an address.")
71+
72+
73+
@app.command(name="history")
74+
async def history(
75+
address: Annotated[
76+
str,
77+
typer.Argument(help="Address of the wallet you want to check / None if you want check your current accounts"),
78+
] = "",
79+
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
80+
private_key_file: Annotated[
81+
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
82+
] = settings.PRIVATE_KEY_FILE,
83+
page_size: Annotated[int, typer.Option(help="Numbers of element per page")] = 100,
84+
page: Annotated[int, typer.Option(help="Current Page")] = 1,
85+
json: Annotated[bool, typer.Option(help="Display as json")] = False,
86+
debug: Annotated[bool, typer.Option()] = False,
87+
):
88+
setup_logging(debug)
89+
90+
account: AccountFromPrivateKey = _load_account(private_key, private_key_file)
91+
92+
if account and not address:
93+
address = account.get_address()
94+
95+
try:
96+
# Comment the original API call for testing
97+
async with AlephHttpClient(api_server=settings.API_HOST) as client:
98+
filtered_credits = await client.get_credit_history(address=address, page_size=page_size, page=page)
99+
if json:
100+
typer.echo(filtered_credits.model_dump_json(indent=4))
101+
else:
102+
table = Table(title="Credits History", border_style="blue", box=box.ROUNDED)
103+
table.add_column("Timestamp")
104+
table.add_column("Amount", justify="right")
105+
table.add_column("Payment Method")
106+
table.add_column("Origin")
107+
table.add_column("Origin Ref")
108+
table.add_column("Expiration Date")
109+
110+
for credit in filtered_credits.credit_balances:
111+
timestamp = Text(credit.message_timestamp.strftime("%Y-%m-%d %H:%M:%S"))
112+
amount = Text(displayable_amount(credit.amount, decimals=2), style="cyan")
113+
payment_method = Text(credit.payment_method if credit.payment_method else "-")
114+
origin = Text(credit.origin if credit.origin else "-")
115+
origin_ref = Text(credit.origin_ref if credit.origin_ref else "-")
116+
expiration = Text(
117+
credit.expiration_date.strftime("%Y-%m-%d") if credit.expiration_date else "Never",
118+
style="red" if credit.expiration_date else "green",
119+
)
120+
121+
table.add_row(timestamp, amount, payment_method, origin, origin_ref, expiration)
122+
123+
# Add pagination footer
124+
pagination_info = Text.assemble(
125+
"Page: ",
126+
Text(f"{filtered_credits.pagination_page}", style="cyan"),
127+
f" of {filtered_credits.pagination_total} | ",
128+
"Items per page: ",
129+
Text(f"{filtered_credits.pagination_per_page}"),
130+
" | ",
131+
"Total items: ",
132+
Text(f"{filtered_credits.pagination_total}"),
133+
)
134+
table.caption = pagination_info
135+
136+
console.print(table)
137+
138+
# Add summary panel
139+
infos = [
140+
Text.from_markup(f"[bold]Address:[/bold] {address}"),
141+
]
142+
console.print(
143+
Panel(
144+
Text.assemble(*infos),
145+
title="Credits Info",
146+
border_style="blue",
147+
expand=False,
148+
title_align="left",
149+
)
150+
)
151+
except ClientResponseError as e:
152+
typer.echo("Failed to retrieve credits history.")
153+
raise (e)

tests/unit/test_credits.py

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from aiohttp import ClientResponseError
77

8-
from aleph_client.commands.credit import show
8+
from aleph_client.commands.credit import history, show
99

1010

1111
@pytest.fixture
@@ -49,6 +49,47 @@ def mock_credits_list_response():
4949
return mock_response
5050

5151

52+
@pytest.fixture
53+
def mock_credit_history_response():
54+
"""Create a mock response for credit history API call."""
55+
mock_response = AsyncMock()
56+
mock_response.__aenter__.return_value = mock_response
57+
mock_response.status = 200
58+
mock_response.json = AsyncMock(
59+
return_value={
60+
"address": "0x1234567890123456789012345678901234567890",
61+
"credit_balances": [
62+
{
63+
"amount": 1000000000,
64+
"message_timestamp": "2023-06-15T12:30:45Z",
65+
"payment_method": "credit_card",
66+
"origin": "purchase",
67+
"origin_ref": "txn_123456",
68+
"expiration_date": "2024-06-15T12:30:45Z",
69+
"credit_ref": "credit_ref_1",
70+
"credit_index": 1,
71+
},
72+
{
73+
"amount": 500000000,
74+
"message_timestamp": "2023-07-20T15:45:30Z",
75+
"payment_method": "wire_transfer",
76+
"origin": "purchase",
77+
"origin_ref": "txn_789012",
78+
"expiration_date": None,
79+
"credit_ref": "credit_ref_2",
80+
"credit_index": 2,
81+
},
82+
],
83+
"pagination_page": 1,
84+
"pagination_total": 1,
85+
"pagination_per_page": 100,
86+
"pagination_total_items": 2,
87+
"pagination_item": "credit_history",
88+
}
89+
)
90+
return mock_response
91+
92+
5293
@pytest.fixture
5394
def mock_credit_error_response():
5495
"""Create a mock error response for credit API calls."""
@@ -89,6 +130,7 @@ async def run(mock_get):
89130
@pytest.mark.asyncio
90131
async def test_show_json_output(mock_credit_balance_response, capsys):
91132
"""Test the show command with JSON output."""
133+
import json
92134

93135
@patch("aiohttp.ClientSession.get")
94136
async def run(mock_get):
@@ -105,8 +147,13 @@ async def run(mock_get):
105147

106148
await run()
107149
captured = capsys.readouterr()
108-
assert "0x1234567890123456789012345678901234567890" in captured.out
109-
assert "1000000000" in captured.out
150+
151+
# Try to parse the output as JSON to validate it's properly formatted
152+
parsed_json = json.loads(captured.out)
153+
154+
# Verify expected data is in the parsed JSON
155+
assert parsed_json["address"] == "0x1234567890123456789012345678901234567890"
156+
assert parsed_json["credits"] == 1000000000
110157

111158

112159
@pytest.mark.asyncio
@@ -181,3 +228,86 @@ async def run(mock_get):
181228
)
182229

183230
await run()
231+
232+
233+
@pytest.mark.asyncio
234+
async def test_history_command(mock_credit_history_response, capsys):
235+
"""Test the history command with an explicit address."""
236+
237+
@patch("aiohttp.ClientSession.get")
238+
async def run(mock_get):
239+
mock_get.return_value = mock_credit_history_response
240+
241+
# Run the history command with an explicit address
242+
await history(
243+
address="0x1234567890123456789012345678901234567890",
244+
private_key=None,
245+
private_key_file=None,
246+
page_size=100,
247+
page=1,
248+
json=False,
249+
debug=False,
250+
)
251+
252+
await run()
253+
captured = capsys.readouterr()
254+
assert "Credits History" in captured.out
255+
assert "0x1234567890123456789012345678901234567890" in captured.out
256+
assert "credit_card" in captured.out
257+
assert "Page: 1" in captured.out
258+
259+
260+
@pytest.mark.asyncio
261+
async def test_history_json_output(mock_credit_history_response, capsys):
262+
"""Test the history command with JSON output."""
263+
import json
264+
265+
@patch("aiohttp.ClientSession.get")
266+
async def run(mock_get):
267+
mock_get.return_value = mock_credit_history_response
268+
269+
# Run the history command with JSON output
270+
await history(
271+
address="0x1234567890123456789012345678901234567890",
272+
private_key=None,
273+
private_key_file=None,
274+
page_size=100,
275+
page=1,
276+
json=True,
277+
debug=False,
278+
)
279+
280+
await run()
281+
captured = capsys.readouterr()
282+
283+
# Try to parse the output as JSON to validate it's properly formatted
284+
parsed_json = json.loads(captured.out)
285+
286+
# Verify expected data is in the parsed JSON
287+
assert parsed_json["address"] == "0x1234567890123456789012345678901234567890"
288+
assert parsed_json["credit_balances"][0]["amount"] == 1000000000
289+
assert parsed_json["credit_balances"][0]["payment_method"] == "credit_card"
290+
assert len(parsed_json["credit_balances"]) == 2
291+
292+
293+
@pytest.mark.asyncio
294+
async def test_history_api_error(mock_credit_error_response):
295+
"""Test the history command handling API errors."""
296+
297+
@patch("aiohttp.ClientSession.get")
298+
async def run(mock_get):
299+
mock_get.return_value = mock_credit_error_response
300+
301+
# Run the history command and expect an exception
302+
with pytest.raises(ClientResponseError):
303+
await history(
304+
address="0x1234567890123456789012345678901234567890",
305+
private_key=None,
306+
private_key_file=None,
307+
page_size=100,
308+
page=1,
309+
json=False,
310+
debug=False,
311+
)
312+
313+
await run()

0 commit comments

Comments
 (0)