prevent negative prices

This commit is contained in:
bob
2026-06-01 11:11:41 -05:00
parent 763305ca89
commit 15310f0fd0

View File

@@ -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",