avios-cli 0.1.6__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {avios_cli-0.1.6 → avios_cli-0.2.0}/CHANGELOG.md +15 -1
  2. {avios_cli-0.1.6 → avios_cli-0.2.0}/PKG-INFO +6 -11
  3. {avios_cli-0.1.6 → avios_cli-0.2.0}/README.md +5 -10
  4. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/__init__.py +1 -1
  5. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/cli.py +27 -23
  6. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/client.py +9 -4
  7. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/config.py +3 -0
  8. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/models.py +18 -6
  9. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/session.py +2 -0
  10. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/app.py +8 -7
  11. avios_cli-0.2.0/tests/fixtures/transactions.json +25 -0
  12. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_cli.py +20 -5
  13. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_client.py +21 -29
  14. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_models.py +21 -2
  15. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_session.py +1 -0
  16. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_tui.py +7 -3
  17. avios_cli-0.1.6/tests/fixtures/transactions.json +0 -5
  18. {avios_cli-0.1.6 → avios_cli-0.2.0}/.editorconfig +0 -0
  19. {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/dependabot.yml +0 -0
  23. {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  24. {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/workflows/publish.yml +0 -0
  25. {avios_cli-0.1.6 → avios_cli-0.2.0}/.gitignore +0 -0
  26. {avios_cli-0.1.6 → avios_cli-0.2.0}/.pre-commit-config.yaml +0 -0
  27. {avios_cli-0.1.6 → avios_cli-0.2.0}/.python-version +0 -0
  28. {avios_cli-0.1.6 → avios_cli-0.2.0}/CONTRIBUTING.md +0 -0
  29. {avios_cli-0.1.6 → avios_cli-0.2.0}/LICENSE +0 -0
  30. {avios_cli-0.1.6 → avios_cli-0.2.0}/SECURITY.md +0 -0
  31. {avios_cli-0.1.6 → avios_cli-0.2.0}/docs/dashboard.svg +0 -0
  32. {avios_cli-0.1.6 → avios_cli-0.2.0}/pyproject.toml +0 -0
  33. {avios_cli-0.1.6 → avios_cli-0.2.0}/scripts/screenshot.py +0 -0
  34. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/auth.py +0 -0
  35. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/endpoints.py +0 -0
  36. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/__init__.py +0 -0
  37. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/art.py +0 -0
  38. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/styles.tcss +0 -0
  39. {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/widgets.py +0 -0
  40. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/conftest.py +0 -0
  41. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/fixtures/balance.json +0 -0
  42. {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_auth.py +0 -0
  43. {avios_cli-0.1.6 → avios_cli-0.2.0}/uv.lock +0 -0
@@ -6,6 +6,19 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-07-24
10
+
11
+ ### Added
12
+ - **`transactions`, `pending` and `overview` now work.** The manage-avios API
13
+ only needed an `x-avios-opco` header (not a separate session, as previously
14
+ thought) — it's now sent automatically. `transactions` renders date /
15
+ description / Avios (coloured +/-) / type and respects `--limit`; the TUI shows
16
+ real transactions too.
17
+ - Typed `Transaction` model (identifier, dateProcessed, description, type,
18
+ partner, amount, categories) and a `NamedRef` helper.
19
+ - `AVIOS_OPCO` setting (default `BAEC` = British Airways Executive Club) for other
20
+ Avios programmes.
21
+
9
22
  ## [0.1.6] - 2026-07-24
10
23
 
11
24
  ### Fixed
@@ -107,7 +120,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
107
120
  - Reward-flight **availability** search is not yet implemented (needs a British
108
121
  Airways capture).
109
122
 
110
- [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.1.6...HEAD
123
+ [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.2.0...HEAD
124
+ [0.2.0]: https://github.com/alexechoi/avios-cli/compare/v0.1.6...v0.2.0
111
125
  [0.1.6]: https://github.com/alexechoi/avios-cli/compare/v0.1.5...v0.1.6
112
126
  [0.1.5]: https://github.com/alexechoi/avios-cli/compare/v0.1.4...v0.1.5
113
127
  [0.1.4]: https://github.com/alexechoi/avios-cli/compare/v0.1.3...v0.1.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avios-cli
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: A CLI and TUI for avios.com — view your Avios balance and transactions from the terminal.
5
5
  Project-URL: Homepage, https://github.com/alexechoi/avios-cli
6
6
  Project-URL: Repository, https://github.com/alexechoi/avios-cli
@@ -120,20 +120,15 @@ expires after ~a day; just run `avios login` again. Run `avios logout` to remove
120
120
 
121
121
  ```bash
122
122
  avios balance # total / individual / household Avios
123
+ avios transactions --limit 20 # recent transactions (date, description, Avios, type)
124
+ avios pending # pending Avios
125
+ avios overview # dashboard summary
123
126
  avios whoami # name, tier, membership, email
124
127
  avios raw /shell/api/users/current/accounts # hit any endpoint directly
125
128
  ```
126
129
 
127
- Add `--json` to `balance` or `whoami` for scriptable output.
128
-
129
- **Experimental** (need the `manage-avios` app session, which the cookie/SSO login
130
- can't yet establish — these currently return "session expired"):
131
-
132
- ```bash
133
- avios transactions --limit 20
134
- avios pending
135
- avios overview
136
- ```
130
+ Add `--json` to `balance`, `transactions`, `pending` or `whoami` for scriptable
131
+ output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
137
132
 
138
133
  ## TUI
139
134
 
@@ -85,20 +85,15 @@ expires after ~a day; just run `avios login` again. Run `avios logout` to remove
85
85
 
86
86
  ```bash
87
87
  avios balance # total / individual / household Avios
88
+ avios transactions --limit 20 # recent transactions (date, description, Avios, type)
89
+ avios pending # pending Avios
90
+ avios overview # dashboard summary
88
91
  avios whoami # name, tier, membership, email
89
92
  avios raw /shell/api/users/current/accounts # hit any endpoint directly
90
93
  ```
91
94
 
92
- Add `--json` to `balance` or `whoami` for scriptable output.
93
-
94
- **Experimental** (need the `manage-avios` app session, which the cookie/SSO login
95
- can't yet establish — these currently return "session expired"):
96
-
97
- ```bash
98
- avios transactions --limit 20
99
- avios pending
100
- avios overview
101
- ```
95
+ Add `--json` to `balance`, `transactions`, `pending` or `whoami` for scriptable
96
+ output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
102
97
 
103
98
  ## TUI
104
99
 
@@ -3,4 +3,4 @@
3
3
  View your Avios balance, transactions and account details from the terminal.
4
4
  """
5
5
 
6
- __version__ = "0.1.6"
6
+ __version__ = "0.2.0"
@@ -20,7 +20,7 @@ from rich.table import Table
20
20
  from avios import __version__
21
21
  from avios.auth import LoginError, import_from_browser, login_via_browser
22
22
  from avios.client import AviosClient
23
- from avios.models import Balance
23
+ from avios.models import Balance, Transaction
24
24
  from avios.session import NotAuthenticated, Session, SessionExpired
25
25
 
26
26
  app = typer.Typer(
@@ -78,19 +78,6 @@ def _print_json(data: Any) -> None:
78
78
  console.print_json(data=data)
79
79
 
80
80
 
81
- def _render_records(records: list[dict[str, Any]], title: str) -> None:
82
- if not records:
83
- console.print(f"[dim]No {title.lower()}.[/]")
84
- return
85
- keys = [k for k, v in records[0].items() if not isinstance(v, dict | list)][:6]
86
- table = Table(title=title)
87
- for key in keys:
88
- table.add_column(key)
89
- for record in records:
90
- table.add_row(*[str(record.get(key, "")) for key in keys])
91
- console.print(table)
92
-
93
-
94
81
  # -- auth commands -----------------------------------------------------------
95
82
  @app.command()
96
83
  def login(
@@ -172,36 +159,53 @@ def _render_balance(result: Balance) -> None:
172
159
  console.print(table)
173
160
 
174
161
 
162
+ def _render_transactions(items: list[Transaction], title: str) -> None:
163
+ if not items:
164
+ console.print(f"[dim]No {title.lower()}.[/]")
165
+ return
166
+ table = Table(title=title)
167
+ table.add_column("Date")
168
+ table.add_column("Description")
169
+ table.add_column("Avios", justify="right")
170
+ table.add_column("Type")
171
+ for txn in items:
172
+ date = (txn.date_processed or "")[:10]
173
+ desc = (txn.description or "").splitlines()[0][:44] if txn.description else ""
174
+ amount = f"{txn.amount:+,}" if txn.amount is not None else ""
175
+ colour = "green" if (txn.amount or 0) >= 0 else "red"
176
+ kind = txn.type.value if txn.type else ""
177
+ table.add_row(date, desc, f"[{colour}]{amount}[/]", kind or "")
178
+ console.print(table)
179
+
180
+
175
181
  @app.command()
176
182
  def transactions(
177
183
  limit: int = typer.Option(20, help="Number of transactions to show."),
178
184
  json_out: bool = JSON_OPTION,
179
185
  ) -> None:
180
- """List recent Avios transactions (experimental; needs the manage-avios session)."""
186
+ """List recent Avios transactions."""
181
187
  with _handle_errors():
182
188
  items = _client().get_transactions(limit=limit)
183
- records = [item.as_dict() for item in items]
184
189
  if json_out:
185
- _print_json(records)
190
+ _print_json([item.as_dict() for item in items])
186
191
  return
187
- _render_records(records, "Transactions")
192
+ _render_transactions(items, "Transactions")
188
193
 
189
194
 
190
195
  @app.command()
191
196
  def pending(json_out: bool = JSON_OPTION) -> None:
192
- """List pending Avios transactions (experimental; needs the manage-avios session)."""
197
+ """List pending Avios transactions."""
193
198
  with _handle_errors():
194
199
  items = _client().get_pending_transactions()
195
- records = [item.as_dict() for item in items]
196
200
  if json_out:
197
- _print_json(records)
201
+ _print_json([item.as_dict() for item in items])
198
202
  return
199
- _render_records(records, "Pending")
203
+ _render_transactions(items, "Pending")
200
204
 
201
205
 
202
206
  @app.command()
203
207
  def overview() -> None:
204
- """Show the dashboard overview (experimental; needs the manage-avios session)."""
208
+ """Show the dashboard overview (raw JSON)."""
205
209
  with _handle_errors():
206
210
  result = _client().get_overview()
207
211
  _print_json(result.as_dict())
@@ -47,11 +47,16 @@ class AviosClient:
47
47
  def get_overview(self) -> Overview:
48
48
  return Overview.model_validate(self.session.get_json(endpoints.OVERVIEW))
49
49
 
50
- def get_transactions(self, limit: int | None = None) -> list[Transaction]:
51
- payload = self.session.get_json(endpoints.TRANSACTIONS)
52
- items = _extract_list(payload)
50
+ def get_transactions(self, limit: int = 50) -> list[Transaction]:
51
+ # `offset` is a server-side page hint; slice to `limit` for an exact count.
52
+ offset = limit if limit and limit > 0 else 1000
53
+ path = f"{endpoints.TRANSACTIONS}?startRecord=1&offset={offset}&status=completed"
54
+ payload = self.session.get_json(path)
55
+ items = (
56
+ payload.get("transactions", []) if isinstance(payload, dict) else _extract_list(payload)
57
+ )
53
58
  transactions = [Transaction.model_validate(item) for item in items]
54
- return transactions[:limit] if limit is not None else transactions
59
+ return transactions[:limit] if limit and limit > 0 else transactions
55
60
 
56
61
  def get_pending_transactions(self) -> list[Transaction]:
57
62
  payload = self.session.get_json(endpoints.TRANSACTIONS_PENDING)
@@ -39,6 +39,9 @@ class Settings(BaseSettings):
39
39
  user_agent: str = DEFAULT_USER_AGENT
40
40
  config_dir: Path = Field(default_factory=_default_config_dir)
41
41
  request_timeout: float = 20.0
42
+ # Operating company code sent as `x-avios-opco`; required by the manage-avios
43
+ # API. "BAEC" = British Airways Executive Club (override with AVIOS_OPCO).
44
+ opco: str = "BAEC"
42
45
 
43
46
  @property
44
47
  def state_path(self) -> Path:
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  from typing import Any
13
13
 
14
- from pydantic import BaseModel, ConfigDict
14
+ from pydantic import BaseModel, ConfigDict, Field
15
15
 
16
16
 
17
17
  class AviosModel(BaseModel):
@@ -35,12 +35,24 @@ class Balance(AviosModel):
35
35
  household: int | None = None
36
36
 
37
37
 
38
- class Transaction(AviosModel):
39
- """A single Avios statement entry.
38
+ class NamedRef(AviosModel):
39
+ """A ``{value, id}`` reference (transaction type, partner, ...)."""
40
40
 
41
- Concrete fields are pinned once live transaction payloads are observed; until
42
- then every key is preserved via ``extra='allow'``.
43
- """
41
+ value: str | None = None
42
+ id: str | None = None
43
+
44
+
45
+ class Transaction(AviosModel):
46
+ """A single Avios statement entry (from the manage-avios transactions API)."""
47
+
48
+ identifier: str | None = None
49
+ date_processed: str | None = Field(default=None, alias="dateProcessed")
50
+ description: str | None = None
51
+ type: NamedRef | None = None
52
+ partner: NamedRef | None = None
53
+ amount: int | None = None
54
+ categories: list[str] = Field(default_factory=list)
55
+ record_index: int | None = Field(default=None, alias="recordIndex")
44
56
 
45
57
 
46
58
  class Account(AviosModel):
@@ -95,6 +95,8 @@ class Session:
95
95
  "accept": "application/json, text/plain, */*",
96
96
  "user-agent": self.settings.user_agent,
97
97
  "referer": f"{self.settings.base_url}/manage-avios/dashboard",
98
+ # Required by the manage-avios API (401 without it).
99
+ "x-avios-opco": self.settings.opco,
98
100
  "cookie": cookie,
99
101
  }
100
102
  return httpx.Client(
@@ -21,7 +21,6 @@ from avios.tui.art import banner_text
21
21
  from avios.tui.widgets import BalanceDisplay
22
22
 
23
23
  TRANSACTIONS_TO_SHOW = 50
24
- MAX_COLUMNS = 6
25
24
 
26
25
 
27
26
  class AviosApp(App[None]):
@@ -87,15 +86,17 @@ class AviosApp(App[None]):
87
86
  table = self.query_one("#transactions", DataTable)
88
87
  table.loading = False
89
88
  table.clear(columns=True)
90
- records = [txn.as_dict() for txn in transactions]
91
- if not records:
89
+ if not transactions:
92
90
  table.add_column("info")
93
91
  table.add_row("No transactions")
94
92
  return
95
- keys = [k for k, v in records[0].items() if not isinstance(v, dict | list)][:MAX_COLUMNS]
96
- table.add_columns(*keys)
97
- for record in records:
98
- table.add_row(*[str(record.get(key, "")) for key in keys])
93
+ table.add_columns("Date", "Description", "Avios", "Type")
94
+ for txn in transactions:
95
+ date = (txn.date_processed or "")[:10]
96
+ desc = (txn.description or "").splitlines()[0][:44] if txn.description else ""
97
+ amount = f"{txn.amount:+,}" if txn.amount is not None else ""
98
+ kind = txn.type.value if txn.type else ""
99
+ table.add_row(date, desc, amount, kind)
99
100
 
100
101
 
101
102
  def run(client: AviosClient | None = None) -> None:
@@ -0,0 +1,25 @@
1
+ {
2
+ "totalRecords": 2,
3
+ "transactions": [
4
+ {
5
+ "identifier": "2139440378",
6
+ "dateProcessed": "2026-07-21T11:37:35.000Z",
7
+ "description": "UBER",
8
+ "type": {"value": "Collection", "id": "DV_COL"},
9
+ "partner": {"value": "UBER", "id": "UBR"},
10
+ "amount": 50,
11
+ "categories": ["collectingAvios"],
12
+ "recordIndex": 1
13
+ },
14
+ {
15
+ "identifier": "2023965591",
16
+ "dateProcessed": "2025-12-10T21:37:04.000Z",
17
+ "description": "Tokyo Haneda to London Heathrow\nClass: Club World\nBA0008",
18
+ "type": {"value": "Reward Flight", "id": "FLT_RWD"},
19
+ "partner": {"value": "British Airways", "id": "BA"},
20
+ "amount": -52000,
21
+ "categories": ["flights"],
22
+ "recordIndex": 2
23
+ }
24
+ ]
25
+ }
@@ -30,12 +30,26 @@ class FakeClient:
30
30
  def get_balance(self) -> Balance:
31
31
  return Balance(balance=100, individual=100, household=200)
32
32
 
33
- def get_transactions(self, limit: int | None = None) -> list[Transaction]:
33
+ def get_transactions(self, limit: int = 50) -> list[Transaction]:
34
34
  items = [
35
- Transaction.model_validate({"date": "2026-07-01", "avios": 100}),
36
- Transaction.model_validate({"date": "2026-06-01", "avios": 50}),
35
+ Transaction.model_validate(
36
+ {
37
+ "dateProcessed": "2026-07-01T09:00:00.000Z",
38
+ "description": "UBER",
39
+ "type": {"value": "Collection", "id": "DV_COL"},
40
+ "amount": 100,
41
+ }
42
+ ),
43
+ Transaction.model_validate(
44
+ {
45
+ "dateProcessed": "2026-06-01T09:00:00.000Z",
46
+ "description": "Deliveroo UK",
47
+ "type": {"value": "BA Avios eStore", "id": "BA_AV_ESTORE"},
48
+ "amount": 50,
49
+ }
50
+ ),
37
51
  ]
38
- return items[:limit] if limit is not None else items
52
+ return items[:limit]
39
53
 
40
54
  def get_pending_transactions(self) -> list[Transaction]:
41
55
  return []
@@ -100,7 +114,8 @@ def test_transactions_respects_limit(fake_client: None) -> None:
100
114
  def test_transactions_json(fake_client: None) -> None:
101
115
  result = runner.invoke(app, ["transactions", "--json"])
102
116
  assert result.exit_code == 0
103
- assert '"avios"' in result.stdout
117
+ assert '"amount"' in result.stdout
118
+ assert '"dateProcessed"' in result.stdout
104
119
 
105
120
 
106
121
  def test_whoami(fake_client: None) -> None:
@@ -1,9 +1,4 @@
1
- """Tests for the typed API client.
2
-
3
- The transactions/accounts fixtures are representative, not confirmed shapes, so
4
- these tests assert on counts and field pass-through (via ``extra='allow'``) rather
5
- than a rigid schema. The balance shape is confirmed.
6
- """
1
+ """Tests for the typed API client (balance, transactions, profile, ...)."""
7
2
 
8
3
  from __future__ import annotations
9
4
 
@@ -19,8 +14,12 @@ from avios.config import Settings
19
14
  from avios.session import Session, SessionExpired
20
15
 
21
16
 
22
- def _client(settings: Settings, routes: dict[str, Any]) -> AviosClient:
17
+ def _client(
18
+ settings: Settings, routes: dict[str, Any], captured: list[httpx.Request] | None = None
19
+ ) -> AviosClient:
23
20
  def handler(request: httpx.Request) -> httpx.Response:
21
+ if captured is not None:
22
+ captured.append(request)
24
23
  if request.url.path not in routes:
25
24
  return httpx.Response(404, json={})
26
25
  body = routes[request.url.path]
@@ -40,28 +39,21 @@ def test_get_balance(settings: Settings, load_fixture: Callable[[str], Any]) ->
40
39
  assert balance.household == 112430
41
40
 
42
41
 
43
- def test_get_transactions_from_bare_list(
44
- settings: Settings, load_fixture: Callable[[str], Any]
45
- ) -> None:
46
- client = _client(settings, {endpoints.TRANSACTIONS: load_fixture("transactions.json")})
47
- txns = client.get_transactions()
48
- assert len(txns) == 3
49
- assert txns[0].as_dict()["description"] == "Flight BA286 SFO-LHR"
50
-
51
-
52
- def test_get_transactions_respects_limit(
53
- settings: Settings, load_fixture: Callable[[str], Any]
54
- ) -> None:
55
- client = _client(settings, {endpoints.TRANSACTIONS: load_fixture("transactions.json")})
56
- assert len(client.get_transactions(limit=2)) == 2
57
-
58
-
59
- def test_get_transactions_from_wrapped_object(settings: Settings) -> None:
60
- payload = {"total": 1, "transactions": [{"date": "2026-07-01", "avios": 100}]}
61
- client = _client(settings, {endpoints.TRANSACTIONS: payload})
62
- txns = client.get_transactions()
63
- assert len(txns) == 1
64
- assert txns[0].as_dict()["avios"] == 100
42
+ def test_get_transactions(settings: Settings, load_fixture: Callable[[str], Any]) -> None:
43
+ captured: list[httpx.Request] = []
44
+ client = _client(
45
+ settings, {endpoints.TRANSACTIONS: load_fixture("transactions.json")}, captured
46
+ )
47
+ txns = client.get_transactions(limit=2)
48
+ assert len(txns) == 2
49
+ assert txns[0].type is not None and txns[0].type.value == "Collection"
50
+ assert txns[0].amount == 50
51
+ assert txns[1].amount == -52000
52
+ # Sends the required opco header and the limit as offset.
53
+ req = captured[-1]
54
+ assert req.headers["x-avios-opco"] == "BAEC"
55
+ assert "offset=2" in str(req.url)
56
+ assert "status=completed" in str(req.url)
65
57
 
66
58
 
67
59
  def test_get_profile(settings: Settings) -> None:
@@ -29,6 +29,25 @@ def test_balance_extras_are_optional() -> None:
29
29
  def test_extra_fields_are_preserved() -> None:
30
30
  txn = Transaction.model_validate({"date": "2026-07-01", "avios": 500, "unknown": "x"})
31
31
  dumped = txn.as_dict()
32
- assert dumped["date"] == "2026-07-01"
33
- assert dumped["avios"] == 500
34
32
  assert dumped["unknown"] == "x"
33
+
34
+
35
+ def test_transaction_parses_real_shape() -> None:
36
+ txn = Transaction.model_validate(
37
+ {
38
+ "identifier": "2139440378",
39
+ "dateProcessed": "2026-07-21T11:37:35.000Z",
40
+ "description": "UBER",
41
+ "type": {"value": "Collection", "id": "DV_COL"},
42
+ "partner": {"value": "UBER", "id": "UBR"},
43
+ "amount": 50,
44
+ "categories": ["collectingAvios"],
45
+ "recordIndex": 1,
46
+ }
47
+ )
48
+ assert txn.identifier == "2139440378"
49
+ assert txn.date_processed == "2026-07-21T11:37:35.000Z"
50
+ assert txn.type is not None and txn.type.value == "Collection"
51
+ assert txn.partner is not None and txn.partner.id == "UBR"
52
+ assert txn.amount == 50
53
+ assert txn.categories == ["collectingAvios"]
@@ -67,6 +67,7 @@ def test_client_requires_authentication(settings: Settings) -> None:
67
67
  def test_get_json_returns_parsed_body(settings: Settings) -> None:
68
68
  def handler(request: httpx.Request) -> httpx.Response:
69
69
  assert request.headers["cookie"] == "appSession=abc"
70
+ assert request.headers["x-avios-opco"] == "BAEC"
70
71
  return httpx.Response(200, json={"balance": 75751})
71
72
 
72
73
  session = _session(settings, httpx.MockTransport(handler))
@@ -17,10 +17,14 @@ class FakeClient:
17
17
  def get_balance(self) -> Balance:
18
18
  return Balance(balance=100, individual=100, household=200)
19
19
 
20
- def get_transactions(self, limit: int | None = None) -> list[Transaction]:
20
+ def get_transactions(self, limit: int = 50) -> list[Transaction]:
21
21
  return [
22
- Transaction.model_validate({"date": "2026-07-01", "avios": 100}),
23
- Transaction.model_validate({"date": "2026-06-01", "avios": 50}),
22
+ Transaction.model_validate(
23
+ {"dateProcessed": "2026-07-01T09:00:00Z", "description": "UBER", "amount": 100}
24
+ ),
25
+ Transaction.model_validate(
26
+ {"dateProcessed": "2026-06-01T09:00:00Z", "description": "Flight", "amount": -5000}
27
+ ),
24
28
  ]
25
29
 
26
30
 
@@ -1,5 +0,0 @@
1
- [
2
- {"date": "2026-07-01", "description": "Flight BA286 SFO-LHR", "avios": 4200},
3
- {"date": "2026-06-15", "description": "Tesco groceries", "avios": 150},
4
- {"date": "2026-06-02", "description": "Redemption: LHR-JFK", "avios": -50000}
5
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes