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>
81 lines
2.9 KiB
C#
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 };
|
|
}
|
|
}
|