Files
EbayListingTool/EbayListingTool/Services/EbayPriceResearchService.cs
Peter Foster 551bed6814 Add live eBay Browse API price lookup to photo analysis
After photo analysis completes, fires a background request to the
eBay Browse API (app-level auth, no user login needed) to fetch
current Buy It Now prices for similar items on eBay UK.

- EbayAuthService.GetAppTokenAsync(): client credentials OAuth flow,
  cached for the token lifetime
- EbayPriceResearchService: searches /buy/browse/v1/item_summary/search
  filtered to FIXED_PRICE + EBAY_GB, returns min/max/median/suggested
  (suggested = 40th percentile — competitive but not cheapest)
- PhotoAnalysisView: shows spinner + "Checking live eBay UK prices…",
  then updates price badge, range bar and PriceOverride with real data,
  plus a "Based on N live listings (£X – £Y)" label

Silently degrades if eBay credentials are not configured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:05:16 +01:00

81 lines
2.9 KiB
C#

using System.Net.Http.Headers;
using Newtonsoft.Json.Linq;
namespace EbayListingTool.Services;
public class LivePriceResult
{
public List<decimal> Prices { get; init; } = new();
public int Count => Prices.Count;
public decimal Min => Prices.Count > 0 ? Prices.Min() : 0;
public decimal Max => Prices.Count > 0 ? Prices.Max() : 0;
public decimal Median => Prices.Count > 0 ? Percentile(50) : 0;
public decimal Suggested => Prices.Count > 0 ? Percentile(40) : 0; // 40th pct: competitive but not lowest
private decimal Percentile(int pct)
{
var sorted = Prices.OrderBy(p => p).ToList();
int idx = (int)Math.Round((pct / 100.0) * (sorted.Count - 1));
return sorted[Math.Clamp(idx, 0, sorted.Count - 1)];
}
}
/// <summary>
/// Uses the eBay Browse API (app-level token, no user login required) to fetch
/// current Buy It Now prices for similar items on eBay UK.
/// </summary>
public class EbayPriceResearchService
{
private readonly EbayAuthService _auth;
private static readonly HttpClient _http = new();
public EbayPriceResearchService(EbayAuthService auth)
{
_auth = auth;
}
public async Task<LivePriceResult> GetLivePricesAsync(string query, int limit = 20)
{
if (string.IsNullOrWhiteSpace(query))
return new LivePriceResult();
var token = await _auth.GetAppTokenAsync();
var baseUrl = _auth.BaseUrl;
using var request = new HttpRequestMessage(HttpMethod.Get,
$"{baseUrl}/buy/browse/v1/item_summary/search" +
$"?q={Uri.EscapeDataString(query)}" +
$"&filter=buyingOptions%3A%7BFIXED_PRICE%7D" +
$"&limit={limit}" +
$"&sort=price");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.Add("X-EBAY-C-MARKETPLACE-ID", "EBAY_GB");
var response = await _http.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"eBay Browse API error ({(int)response.StatusCode}): {json}");
var obj = JObject.Parse(json);
var items = obj["itemSummaries"] as JArray ?? new JArray();
var prices = new List<decimal>();
foreach (var item in items)
{
var priceVal = item["price"]?["value"]?.ToString();
var currency = item["price"]?["currency"]?.ToString();
if (currency == "GBP" && decimal.TryParse(priceVal,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var p) && p > 0)
{
prices.Add(p);
}
}
return new LivePriceResult { Prices = prices };
}
}