prevent negative prices
This commit is contained in:
@@ -81,13 +81,46 @@ public sealed class CsMoneyIngestService
|
||||
|| string.Equals(a.Quality, expectedQuality, StringComparison.OrdinalIgnoreCase);
|
||||
}).ToList();
|
||||
|
||||
var skipped = items.Count - matched.Count;
|
||||
if (matched.Count == 0)
|
||||
// Of the name/wear matches, keep only listings with a usable price. cs.money's
|
||||
// pricing.default is the DISCOUNTED display price and occasionally arrives <= 0
|
||||
// (its discount math underflows, or a price-less render returns 0); a non-positive
|
||||
// asking price is impossible and would poison the cheapest-price point downstream.
|
||||
// Fall back to priceBeforeDiscount when default isn't positive, and drop the
|
||||
// listing only when neither is — counting those as skipped like a filter miss.
|
||||
var priced = new List<(CsMoneyItem Item, decimal Price)>(matched.Count);
|
||||
var droppedNoPrice = 0;
|
||||
var pricedByFallback = 0;
|
||||
foreach (var it in matched)
|
||||
{
|
||||
// Nothing for this skin+wear. If the sweep was complete this is genuine
|
||||
// (none listed, or a name mismatch) — stamp the checkpoint so it advances.
|
||||
// If it was partial (e.g. challenged before any item), leave it un-stamped
|
||||
// so the band is retried.
|
||||
if (ResolvePrice(it.Pricing) is not { } px)
|
||||
{
|
||||
droppedNoPrice++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(it.Pricing!.Default > 0m))
|
||||
{
|
||||
pricedByFallback++;
|
||||
}
|
||||
|
||||
priced.Add((it, px));
|
||||
}
|
||||
|
||||
if (pricedByFallback > 0 || droppedNoPrice > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"cs.money non-positive default price for {Skin}: {Fallback} used priceBeforeDiscount, "
|
||||
+ "{Dropped} dropped (no usable price).",
|
||||
skin.Name, pricedByFallback, droppedNoPrice);
|
||||
}
|
||||
|
||||
var skipped = items.Count - priced.Count;
|
||||
if (priced.Count == 0)
|
||||
{
|
||||
// Nothing usable for this skin+wear. If the sweep was complete this is genuine
|
||||
// (none listed, a name mismatch, or no usable price) — stamp the checkpoint so
|
||||
// it advances. If it was partial (e.g. challenged before any item), leave it
|
||||
// un-stamped so the band is retried.
|
||||
if (complete)
|
||||
{
|
||||
await StampCheckpointAsync(conditionId, now, ct);
|
||||
@@ -97,7 +130,7 @@ public sealed class CsMoneyIngestService
|
||||
return new CsMoneyIngestResult(0, 0, 0, 0, skipped);
|
||||
}
|
||||
|
||||
var sellOrderIds = matched.Select(it => it.Id).ToList();
|
||||
var sellOrderIds = priced.Select(p => p.Item.Id).ToList();
|
||||
var existing = await _db.CsMoneyListings
|
||||
.Where(l => sellOrderIds.Contains(l.SellOrderId))
|
||||
.ToDictionaryAsync(l => l.SellOrderId, ct);
|
||||
@@ -107,7 +140,7 @@ public sealed class CsMoneyIngestService
|
||||
var touched = new HashSet<long>();
|
||||
var touchedInstanceIds = new HashSet<int>();
|
||||
|
||||
foreach (var it in matched)
|
||||
foreach (var (it, price) in priced)
|
||||
{
|
||||
touched.Add(it.Id);
|
||||
var instance = await ResolveInstanceAsync(skinId, conditionId, it, now, ct);
|
||||
@@ -118,7 +151,7 @@ public sealed class CsMoneyIngestService
|
||||
|
||||
if (existing.TryGetValue(it.Id, out var row))
|
||||
{
|
||||
row.Price = it.Pricing?.Default ?? row.Price;
|
||||
row.Price = price;
|
||||
row.PriceBeforeDiscount = it.Pricing?.PriceBeforeDiscount;
|
||||
row.ComputedPrice = it.Pricing?.Computed;
|
||||
row.AssetId = it.Asset?.Id?.ToString();
|
||||
@@ -131,7 +164,7 @@ public sealed class CsMoneyIngestService
|
||||
}
|
||||
else
|
||||
{
|
||||
var entity = Map(it, skinId, conditionId, now);
|
||||
var entity = Map(it, price, skinId, conditionId, now);
|
||||
entity.SkinInstance = instance;
|
||||
_db.CsMoneyListings.Add(entity);
|
||||
inserted++;
|
||||
@@ -155,7 +188,7 @@ public sealed class CsMoneyIngestService
|
||||
// Record a price point (the cheapest live listing) for this skin+wear.
|
||||
if (conditionId is { } condId)
|
||||
{
|
||||
var minPrice = matched.Where(m => m.Pricing is not null).Select(m => m.Pricing!.Default).Min();
|
||||
var minPrice = priced.Min(p => p.Price);
|
||||
await _db.PriceHistories.AddAsync(new PriceHistory
|
||||
{
|
||||
SkinId = skinId,
|
||||
@@ -175,10 +208,10 @@ public sealed class CsMoneyIngestService
|
||||
_logger.LogInformation(
|
||||
"cs.money ingest {Weapon} | {Skin} ({Wear}): {Matched} matched ({Ins} new, {Upd} upd, "
|
||||
+ "{Rem} removed), {Skipped} skipped by filter{Partial}.",
|
||||
skin.Weapon, skin.Name, conditionName ?? "all", matched.Count, inserted, updated, removed, skipped,
|
||||
skin.Weapon, skin.Name, conditionName ?? "all", priced.Count, inserted, updated, removed, skipped,
|
||||
complete ? "" : " [PARTIAL — not pruned/checkpointed]");
|
||||
|
||||
return new CsMoneyIngestResult(matched.Count, inserted, updated, removed, skipped);
|
||||
return new CsMoneyIngestResult(priced.Count, inserted, updated, removed, skipped);
|
||||
}
|
||||
|
||||
// Find the physical item matching this listing's fingerprint, or create one.
|
||||
@@ -290,7 +323,31 @@ public sealed class CsMoneyIngestService
|
||||
}
|
||||
}
|
||||
|
||||
private static CsMoneyListing Map(CsMoneyItem it, int skinId, int? conditionId, DateTimeOffset now) => new()
|
||||
// The effective asking price for a listing, or null when none is usable. cs.money's
|
||||
// pricing.default is the DISCOUNTED price and occasionally comes back <= 0 (discount
|
||||
// underflow, or a price-less render); fall back to the positive priceBeforeDiscount
|
||||
// (conservative — never understates cost) and give up only when neither is positive.
|
||||
private static decimal? ResolvePrice(CsMoneyPricing? pricing)
|
||||
{
|
||||
if (pricing is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pricing.Default > 0m)
|
||||
{
|
||||
return pricing.Default;
|
||||
}
|
||||
|
||||
if (pricing.PriceBeforeDiscount is { } before && before > 0m)
|
||||
{
|
||||
return before;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CsMoneyListing Map(CsMoneyItem it, decimal price, int skinId, int? conditionId, DateTimeOffset now) => new()
|
||||
{
|
||||
SellOrderId = it.Id,
|
||||
AssetId = it.Asset?.Id?.ToString(),
|
||||
@@ -304,7 +361,7 @@ public sealed class CsMoneyIngestService
|
||||
IsStatTrak = it.Asset?.IsStatTrak ?? false,
|
||||
IsSouvenir = it.Asset?.IsSouvenir ?? false,
|
||||
StickerCount = it.Stickers?.Count(s => s is not null) ?? 0,
|
||||
Price = it.Pricing?.Default ?? 0m,
|
||||
Price = price,
|
||||
PriceBeforeDiscount = it.Pricing?.PriceBeforeDiscount,
|
||||
ComputedPrice = it.Pricing?.Computed,
|
||||
Currency = "USD",
|
||||
|
||||
Reference in New Issue
Block a user