From 15310f0fd077801304ae14494cd41b27dc7cf001 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 1 Jun 2026 11:11:41 -0500 Subject: [PATCH] prevent negative prices --- .../CsMoney/CsMoneyIngestService.cs | 87 +++++++++++++++---- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/BlueLaminate/BlueLaminate.Core/CsMoney/CsMoneyIngestService.cs b/BlueLaminate/BlueLaminate.Core/CsMoney/CsMoneyIngestService.cs index 0fa10ca..652f3f0 100644 --- a/BlueLaminate/BlueLaminate.Core/CsMoney/CsMoneyIngestService.cs +++ b/BlueLaminate/BlueLaminate.Core/CsMoney/CsMoneyIngestService.cs @@ -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(); var touchedInstanceIds = new HashSet(); - 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",