Skip to content

Add PointsPath integration: award prices joined to Matrix cash itineraries#1

Merged
ak2k merged 3 commits into
mainfrom
feat/pp-integration
May 15, 2026
Merged

Add PointsPath integration: award prices joined to Matrix cash itineraries#1
ak2k merged 3 commits into
mainfrom
feat/pp-integration

Conversation

@ak2k

@ak2k ak2k commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new --pp flag on flight fare that fetches PointsPath award prices for the same route and joins them onto the Matrix cash itineraries client-side, plus a small flight auth pp subgroup for token management.

$ flight fare JFK LHR --dep 2026-06-09 --pp --pp-airlines United,Delta,American
... (existing Matrix tables) ...
                   Cash + award (matched on flight # × date)
┃ flight ┃     price ┃        Economy ┃        Business ┃ ¢/mi (Y) ┃ funded by ┃
│ AA106  │ USD675.00 │ 30.0k American │ 272.0k American │     2.2¢ │ Citi      │
│        │           │           + $6 │            + $6 │          │           │
│ BA178  │ USD778.00 │ 30.0k American │               — │     1.6¢ │ Citi      │
│        │           │         + $308 │                 │          │           │

The AA106 vs BA178 contrast is the value-add: same airline-of-record award, but the BA-operated leg pulls $308 in fuel surcharges, dropping value from 2.2¢ to 1.6¢/mi.

Architecture

New src/flight_cli/pp/ package:

File Role
models.py Pydantic v2 shapes for airline-search + pricing-info (with null→[] coercion)
auth.py Token store at ~/.config/flight-cli/pp.json, Supabase JWT refresh, JWT decode helpers
client.py Async httpx client; semaphore-bounded fan-out (default 5); 401→refresh→retry; 24h on-disk cache for pricing-info
match.py Outer-join cash itineraries to award flights by (flight_number, departure_date)
cli.py auth pp Typer subapp (login/whoami/logout) + run_pp_for_search augmenter + Rich render

Edits to existing src/flight_cli/cli.py are minimal: register the auth subapp, add 4 flags to fare, and call the augmenter when --pp is set. Existing commands are untouched.

API surface (reverse-engineered from extension traffic)

  • POST /api/airline-search — one POST per airline; returns outboundFlights[] with perCabinMilesPricing per flight
  • GET /api/pricing-info — airline → transfer-partner banks + active bonus ratios + expiry
  • Auth: Supabase JWT Authorization: Bearer ...; refresh via https://hxjqzkcirzhjvtubefie.supabase.co/auth/v1/token
  • Public anon key bundled (extracted from extension chunk; expires 2034); PP_SUPABASE_ANON_KEY env var overrides

Auth

flight auth pp login --tokens-file <path-to-json>   # only login mode today
flight auth pp whoami
flight auth pp logout

Browser-based login is deferred — needs Playwright + the CDP cookie-sniff flow we used to bootstrap. For now, capture tokens from a logged-in Chrome session and pass them as JSON. --help documents this.

Deferred (intentionally not in this PR)

  • Browser-based auth pp login (Playwright integration)
  • calendar --pp (fan-out is N days × M airlines; deserves its own design pass)
  • Multi-leg / round-trip PP queries (today uses leg 0 only)
  • Tests
  • README updates (waiting until shape settles)
  • Dynamic airline catalog (today defaults to 13 airlines we observed firing for international US-EU search; PointsPath's enabled set per Pro tier likely differs)

Test plan

  • flight auth pp login --tokens-file /tmp/pp_tokens.json
  • flight auth pp whoami decodes JWT, prints email + sub + expiry ✓
  • flight auth pp logout removes the store file ✓
  • flight fare JFK LHR --dep 2026-06-09 --pp --pp-airlines United,Delta,American → joined table with correct ¢/mi (verified 2.2¢ for AA award, 1.6¢ for AA-via-BA codeshare with surcharges) ✓
  • pricing-info cache populated at ~/.cache/flight-cli/pp_pricing.json, second invocation hits cache ✓
  • All existing commands' help output unchanged when --pp is not used
  • Token refresh path (didn't exercise; needs an expired token to verify Supabase 200 path)
  • 401 → refresh → retry path (didn't exercise)
  • Round-trip with --return — currently uses leg 0 dep_date only; return leg ignored

ak2k added 3 commits May 15, 2026 03:30
…aries

New `--pp` flag on `flight fare` augments the cash table with award prices
fetched from PointsPath's per-airline `/api/airline-search` endpoint, joined
client-side on (flight number, departure date). Funding banks come from
`/api/pricing-info` (cached 24h). Computes ¢/mi against cash prices.

Auth lives under a new `flight auth pp` subgroup (login / whoami / logout)
to leave room for additional providers later. Today only `--tokens-file`
import works; browser-based login deferred to a follow-up.

Deferred: calendar mode, multi-leg / round-trip PP queries, tests, README.
* Round-trip / multi-city: per-leg PP queries via new LegQuery, one table per
  leg labeled outbound/return/leg N. Matcher takes slice_index so the return
  leg matches against Itinerary.slices[1] etc. Added per-leg dedupe so
  Matrix's NxN itinerary cross-product collapses to one row per unique
  (flight#, departure_date), keeping the cheapest cash pairing.

* Dynamic airline catalog: GET /api/extension-config (cached 7d) feature flags
  intersected with /api/pricing-info airline universe. Always-on airlines
  (no enable<X> flag) included; explicitly disabled (enable<X>=0) excluded;
  versioned flags (enableAirFranceV2) handled via regex. --pp-airlines still
  overrides for explicit selection. Static DEFAULT_AIRLINES kept as
  last-resort fallback if discovery fails.

* Tests: 60 pytest cases covering null-coercion in models, key normalization
  + outer-join semantics in matcher, currency-string parsing variants,
  airline-flag pattern matching. Fixtures are tiny hand-curated JSON; no
  live API hits.

* README: PointsPath section documenting --pp / --pp-only / --pp-airlines /
  --pp-cabin flags, auth setup, airline-discovery rule, and what's deferred.
After rebasing onto main's adopted conventions, the pp/* and tests/pp/*
code needed a localized cleanup pass to satisfy the strict ruleset.
All behavior-preserving:

* Logging: stdlib `logging.getLogger` → `structlog.get_logger` typed as
  BoundLogger via TYPE_CHECKING. Printf-style log calls rewritten as
  key-value events (e.g. `log.warning("pp_airline_search_failed",
  airline=..., status=..., body=...)`).

* Async: asyncio.Semaphore → anyio.Semaphore; asyncio.gather → anyio
  TaskGroup (cleaner per-task error capture); asyncio.run → anyio.run
  via a closure for kwargs-bearing _gather_pp call.

* HTTP boundaries: bare 200/204/400/401 → http.HTTPStatus.* enum
  constants.

* Pydantic config: pp.models._Loose flipped extra="allow" →
  extra="ignore" to match the main models._Loose Profile-B-edge
  convention (PointsPath is reverse-engineered, same justification as
  Matrix responses).

* pp/models.py added to pyproject.toml's N815 per-file-ignore — mirrors
  PointsPath JSON shape with camelCase attrs (same shape as wire.py).

* Imports: app-local types used only in annotations moved to
  TYPE_CHECKING in match.py / client.py / cli.py. Exception: `Path`
  stays at runtime in pp/cli.py — typer evaluates Annotated[Path|None,
  typer.Option(...)] at runtime, so TYPE_CHECKING breaks the CLI.

* Annotations: bare `dict` → `dict[str, Any]` via _JsonDict aliases;
  added types to test parametrize fixtures; B904 `from e` on re-raises;
  HTTPStatus import.

* Strict shims: `# pyright: reportPrivateUsage=false` on tests/pp/
  test_cli.py (intentional access to _normalize_cabin/_parse_cash/
  _parse_csv); `# pyright: reportCallIssue=false` on tests/pp/
  test_match.py (pydantic Field-with-alias confuses the constructor-
  kwargs check despite populate_by_name=True).

* cli.py: reuses the existing _ROUND_TRIP_LEGS constant; lazy fli/
  fast_flights imports kept.

* Unicode: ambiguous `×` replaced with `x` in docstrings + titles to
  satisfy RUF001/RUF002 (consistent with the Carrier-x-stops convention
  PR #2 established).

`make check`: 0 errors / 0 warnings / 69 tests passing. Live `flight
fare JFK LHR` smoke test green (10 solutions, anyio runtime,
ConsoleRenderer log output on stderr).
@ak2k ak2k force-pushed the feat/pp-integration branch from 8a6b345 to be9a02d Compare May 15, 2026 07:52
@ak2k ak2k merged commit 766e02e into main May 15, 2026
1 of 2 checks passed
@ak2k ak2k deleted the feat/pp-integration branch May 15, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant