Make PriceCharting runs auditable and pin matches by product id

Two changes aimed at the first real run, since the integration cannot be
exercised here without a subscription.

Refresh now reports which product each game matched — name, console and the
source's id — next to the prices, and dry-run surfaces it before anything is
written. This is the failure that would otherwise go unnoticed: a lookup for
the DS "Chrono Trigger" resolving to the SNES original returns entirely
plausible numbers for the wrong game, and nothing in a bare price would say
so.

The matched id is then stored on the game, and later refreshes look it up
directly instead of repeating the title search. Cheaper, and stable — a
search that drifts to a different edition next month cannot silently
re-price something that was already matched correctly.

IPriceProvider takes an optional sourceId so this stays provider-agnostic.
eBay ignores it, having no stable per-product identifier in Browse.

139 backend tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 16:52:59 -04:00
co-authored by Claude Opus 5
parent 8e136f42f8
commit d34e6b4ced
10 changed files with 619 additions and 9 deletions
@@ -367,3 +367,60 @@ public class PriceEndpointTests(LudosApiFactory factory) : IClassFixture<LudosAp
decimal? ValueLoose, decimal? ValueCib, decimal? ValueNew);
private record PagePayload(List<GamePayload> Items, int Total);
}
public class PriceChartingMatchTests
{
[Fact]
public void The_matched_product_and_console_are_reported()
{
using var document = JsonDocument.Parse("""
{
"status": "success",
"id": "6910",
"product-name": "Chrono Trigger",
"console-name": "Super Nintendo",
"loose-price": 12800, "cib-price": 65000, "new-price": 1200000
}
""");
var estimate = PriceChartingProvider.Parse(document);
// Without this a dry run cannot tell a DS entry that resolved to the
// SNES original from one that resolved correctly.
Assert.Equal("Chrono Trigger", estimate.MatchedName);
Assert.Equal("Super Nintendo", estimate.MatchedConsole);
Assert.Equal("6910", estimate.SourceId);
}
[Fact]
public void A_numeric_id_is_read_as_a_string()
{
using var document = JsonDocument.Parse("""
{ "status": "success", "id": 6910, "loose-price": 100 }
""");
Assert.Equal("6910", PriceChartingProvider.Parse(document).SourceId);
}
[Fact]
public void Missing_match_metadata_is_not_fatal()
{
using var document = JsonDocument.Parse("""{ "loose-price": 12800 }""");
var estimate = PriceChartingProvider.Parse(document);
Assert.Equal(128.00m, estimate.Loose);
Assert.Null(estimate.MatchedName);
Assert.Null(estimate.SourceId);
}
[Fact]
public void A_stored_id_is_preferred_over_a_title_search()
{
// Documents the intent of the lookup switch: with an id, the query is an
// exact product fetch, so a drifting title search cannot re-price a
// different edition on a later run.
Assert.Equal("super nintendo Chrono Trigger",
PriceChartingProvider.BuildQuery("Chrono Trigger", "SNES"));
}
}