diff --git a/TPP.ArgsParsing/TypeParsers/AnyOrderParser.cs b/TPP.ArgsParsing/TypeParsers/AnyOrderParser.cs index 327b679e..946bce8b 100644 --- a/TPP.ArgsParsing/TypeParsers/AnyOrderParser.cs +++ b/TPP.ArgsParsing/TypeParsers/AnyOrderParser.cs @@ -78,6 +78,8 @@ public async Task> Parse( 2 => typeof(AnyOrder<,>), 3 => typeof(AnyOrder<,,>), 4 => typeof(AnyOrder<,,,>), + 5 => typeof(AnyOrder<,,,,>), + 6 => typeof(AnyOrder<,,,,,>), var num => throw new InvalidOperationException( $"An implementation of {typeof(AnyOrder)} for {num} generic arguments " + "needs to be implemented and wired up where this exception is thrown. " + diff --git a/TPP.ArgsParsing/TypeParsers/BadgeSourceParser.cs b/TPP.ArgsParsing/TypeParsers/BadgeSourceParser.cs new file mode 100644 index 00000000..83b71573 --- /dev/null +++ b/TPP.ArgsParsing/TypeParsers/BadgeSourceParser.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using TPP.Model; + +namespace TPP.ArgsParsing.TypeParsers +{ + public class BadgeSourceParser : IArgumentParser + { + public Task> Parse(IImmutableList args, Type[] genericTypes) + { + string source = args[0]; + ArgsParseResult result; + Badge.BadgeSource? parsedSource = null; + try + { + parsedSource = (Badge.BadgeSource)Enum.Parse(typeof(Badge.BadgeSource), source, ignoreCase: true); + } + catch (ArgumentException) + { + switch (args[1].ToLower()) + { + case "run": + case "caught": + parsedSource = Badge.BadgeSource.RunCaught; + break; + } + } + if (parsedSource != null) + result = ArgsParseResult.Success(parsedSource.Value, args.Skip(1).ToImmutableList()); + else + result = ArgsParseResult.Failure($"Did not find a source named '{args[0]}'"); + return Task.FromResult(result); + } + } +} diff --git a/TPP.ArgsParsing/TypeParsers/FormParser.cs b/TPP.ArgsParsing/TypeParsers/FormParser.cs new file mode 100644 index 00000000..2ffc9310 --- /dev/null +++ b/TPP.ArgsParsing/TypeParsers/FormParser.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TPP.ArgsParsing.Types; + +namespace TPP.ArgsParsing.TypeParsers +{ + public class FormParser : IArgumentParser
+ { + /// + /// Keys and values are in the format: lowercase unspaced name | Capitalized display name. + /// + private static readonly Dictionary Forms = new Dictionary + { + #region common forms + ["alolan"] = "Alolan", + ["alola"] = "Alolan", + ["glarian"] = "Galarian", + ["galar"] = "Galarian", + ["hisuian"] = "Hisuian", //pokemon legends: arceus + ["hisui"] = "Hisuan", + + ["mega"] = "Mega", + ["dynamax"] = "Dyanamax", + ["gigantamax"] = "Gigantamax", + ["normal"] = "Normal", + + ["red"] = "Red", + ["red-stripe"] = "Red", //basculin + ["redstripe"] = "Red", + ["redflower"] = "Red", //Flabébé, Floette, and Florges + ["redcore"] = "Red", //minior + ["blue"] = "Blue", + ["blue-stripe"] = "Blue", + ["bluestripe"] = "Blue", + ["blueflower"] = "Blue", + ["bluecore"] = "Blue", + ["white"] = "White", + ["whiteflower"] = "White", + ["black"] = "Black", + ["orange"] = "Orange", + ["orangeflower"] = "Orange", + ["orangecore"] = "Orange", + ["yellow"] = "Yellow", + ["yellowflower"] = "Yellow", + ["yellowcore"] = "Yellow", + ["green"] = "Green", + ["greencore"] = "Green", + ["indigocore"] = "Indigo", + ["indigo"] = "Indigo", + ["violet"] = "Violet", + ["violetcore"] = "Violet", + #endregion + + #region pokemon specific forms + ["primal"] = "Primal", //Kyogre, Groudon + ["fire"] = "Fire", //Arceus, Silvally + ["water"] = "Water", + ["electric"] = "Electric", + ["grass"] = "Grass", + ["ice"] = "Ice", + ["fighting"] = "fighting", + ["poison"] = "Poison", + ["ground"] = "Ground", + ["psychic"] = "Psychic", + ["flying"] = "Flying", + ["bug"] = "Bug", + ["rock"] = "Rock", + ["ghost"] = "Ghost", + ["dragon"] = "Dragon", + ["dark"] = "Dark", + ["steel"] = "Steel", + ["fairy"] = "Fairy", + ["???"] = "???", + //["normal"] = "Normal", redundant since it is defined above, but left here to show that there is completeness across types + ["cosplay"] = "Cosplay", //pikachu + ["rockstar"] = "Rock Star", + ["belle"] = "Belle", + ["popstar"] = "Pop Star", + ["phd"] = "PHD", + ["dr"] = "PHD", + ["libre"] = "Libre", + ["originalcap"] = "Original Cap", + ["hoenncap"] = "Hoenn Cap", + ["sinnohcap"] = "Sinnoh Cap", + ["unovacap"] = "Unova Cap", + ["kaloscap"] = "Kalos Cap", + ["alolacap"] = "Alola Cap", + ["partnercap"] = "Partner Cap", + ["worldcap"] = "World Cap", + ["spiky-eared"] = "Spiky-Eared", //pichu + ["spikyeared"] = "Spiky-Eared", + ["a"] = "A", //unown + ["b"] = "B", + ["c"] = "C", + ["d"] = "D", + ["e"] = "E", + ["f"] = "F", + ["g"] = "G", + ["h"] = "H", + ["i"] = "I", + ["j"] = "J", + ["k"] = "K", + ["l"] = "L", + ["m"] = "M", + ["n"] = "N", + ["o"] = "O", + ["p"] = "P", + ["q"] = "Q", + ["r"] = "R", + ["s"] = "S", + ["t"] = "T", + ["u"] = "U", + ["v"] = "V", + ["w"] = "W", + ["x"] = "X", + ["y"] = "Y", + ["z"] = "Z", + ["?"] = "?", + ["!"] = "!", + ["sunny"] = "Sunny", //castform + ["rainy"] = "Rainy", + ["rain"] = "Rainy", + ["snowy"] = "Snowy", + ["snow"] = "Snowy", + ["attack"] = "Attack", //deoxys + ["defence"] = "Defence", + ["speed"] = "Speed", + ["plantcloak"] = "Plant Cloak", //burmy & wormadam + ["plant"] = "Plant Cloak", + ["sandycloak"] = "Sandy Cloak", + ["sandcloak"] = "Sandy Cloak", + ["sand"] = "Sandy Cloak", + ["trashcloak"] = "Trash Cloak", + ["trash"] = "Trash Cloak", + ["overcast"] = "Overcast", //cherrim + ["sunshine"] = "Sunshine", + ["westsea"] = "West Sea", //shelos & gastrodon + ["west"] = "West Sea", + ["eastsea"] = "East Sea", + ["east"] = "East Sea", + ["heat"] = "Heat", //rotom + ["wash"] = "Wash", + ["frost"] = "Frost", + ["fan"] = "Fan", + ["mow"] = "Mow", + ["pokedex"] = "Pokédex", + ["phone"] = "phone", + ["origin"] = "Origin", //giritina + ["land"] = "Land", //shaymin + ["sky"] = "Sky", + ["standard"] = "Standard", //darmanitan + ["zen"] = "Zen", + ["galarianstandard"] = "Galarian Standard", + ["galarstandard"] = "Galarian Standard", + ["galarianzen"] = "Galarian Zen", + ["galarzen"] = "Galarian Zen", + ["spring"] = "Spring", //deerling & sawsbuck + ["summer"] = "Summer", + ["autumn"] = "Autumn", + ["fall"] = "Autumn", + ["winter"] = "Winter", + ["incarnate"] = "Incarnate", //tornadus, thundurus, landorus + ["therian"] = "Therian", + ["ordinary"] = "Ordinary", //keldeo + ["resolute"] = "Resoulte", + ["aria"] = "Aria", //Meloetta + ["pirouette"] = "Pirouette", + ["shockdrive"] = "Shock Drive", //genesect + ["shock"] = "Shock Drive", + ["burndrive"] = "Burn Drive", + ["burn"] = "Burn Drive", + ["chilldrive"] = "Chill Drive", + ["chill"] = "Chill Drive", + ["dousedrive"] = "Douse Drive", + ["douse"] = "Douse Drive", + ["ash-greninja"] = "Ash-Greninja", //greninja + ["ashgreninja"] = "Ash-Greninja", + ["ash"] = "Ash-Greninja", + ["archipelago"] = "Archipelago", //vivillon + ["continental"] = "Continental", + ["elegant"] = "Elegant", + ["garden"] = "Garden", + ["highplains"] = "High Plains", + ["icysnow"] = "Icy Snow", + ["icy"] = "Icy Snow", + ["jungle"] = "Jungle", + ["marine"] = "Marine", + ["meadow"] = "Meadow", + ["modern"] = "Modern", + ["monsoon"] = "Monsoon", + ["ocean"] = "Ocean", + ["polar"] = "Polar", + ["river"] = "River", + ["sandstorm"] = "Sandstorm", + ["savanna"] = "Savanna", + ["sun"] = "Sun", + ["tundra"] = "Tundra", + ["pokeball"] = "Poké Ball", + ["fancy"] = "Fancy", + ["natural"] = "Normal", //furfou + ["hearttrim"] = "Heart Trim", + ["heart"] = "Heart Trim", + ["startrim"] = "Star Trim", + ["diamondtrim"] = "Diamond Trim", + ["debutantetrim"] = "Debutante Trim", + ["debutante"] = "Debutante Trim", + ["matrontrim"] = "Matron Trim", + ["matron"] = "Matron Trim", + ["dandytrim"] = "Dandy Trim", + ["lareinetrim"] = "La Reine Trim", + ["lareine"] = "La Reine Trim", + ["kabukitrim"] = "Kabuki Trim", + ["kabuki"] = "Kabuki Trim", + ["pharohtrim"] = "Pharoh Trim", + ["pharoh"] = "Pharoh Trim", + ["shield"] = "Shield", //aegislash, zamazenta + ["crownedshield"] = "Shield", //zamazenta + ["blade"] = "Blade", //aegislash + ["sword"] = "Sword", //zacian + ["crownedsword"] = "Sword", + ["heroofmanybattles"] = "Hero of Many Battles", //zacian & zamazenta + ["hero"] = "Hero of Many Battles", + ["small"] = "Small", //pumpkaboo & gourgeist + ["average"] = "Average", + ["large"] = "Large", + ["super"] = "Super", + ["neutralmode"] = "Neutral Mode", //xerneas + ["neutral"] = "Neutral Mode", + ["activemode"] = "Active Mode", + ["active"] = "Active Mode", + ["cell"] = "Cell", //zygarde + ["core"] = "Core", + ["10%"] = "10%", + ["10percent"] = "10%", + ["50%"] = "50%", + ["50percent"] = "50%", + ["complete"] = "Complete", + ["confined"] = "Confined", //hoopa + ["unbound"] = "Unbound", + ["bailestyle"] = "Baile Style", //oricorio + ["baile"] = "Baile Style", + ["pom-pomstyle"] = "Pom-Pom Style", + ["pompomstyle"] = "Pom-Pom Style", + ["pompom"] = "Pom-Pom Style", + ["pa'ustyle"] = "Pa'u Style", + ["paustyle"] = "Pa'u Style", + ["pau"] = "Pa'u Style", + ["sensustyle"] = "Sensu Style", + ["sensu"] = "Sensu Style", + ["midday"] = "Midday", //lycanroc + ["midnight"] = "Midnight", + ["dusk"] = "Dusk", + ["solo"] = "Solo", //wishiwashi + ["school"] = "School", + ["meteor"] = "Meteor", //minor (color forms above) + ["disguised"] = "Disguised", //mimikyu + ["busted"] = "Busted", + ["duskmane"] = "Dusk", //necrozma + ["dawn"] = "Dawn", + ["dawnwings"] = "Dawn", + ["ultra"] = "Ultra", + ["originalcolor"] = "Original Color", //magearna + ["gulping"] = "Gulping", //cramorant + ["gorging"] = "Gorging", + ["amped"] = "Amped", //toxtricity + ["low-key"] = "Low Key", + ["lowkey"] = "Low Key", + ["phony"] = "Phony", //sinistea & polteageist + ["antique"] = "Antique", + ["strawberryvanilla"] = "Strawberry Vanilla", //alcremie... + ["blueberryvanilla"] = "Blueberry Vanilla", + ["lovevanilla"] = "Love Vanilla", + ["starvanilla"] = "Star Vanilla", + ["clovervanilla"] = "Clover Vanilla", + ["flowervanilla"] = "Flower Vanilla", + ["ribbonvanilla"] = "Ribbon Vanilla", + ["strawberryruby"] = "Strawberry Ruby", + ["blueberryruby"] = "Blueberry Ruby", + ["loveruby"] = "Love Ruby", + ["starruby"] = "Star Ruby", + ["cloverruby"] = "Clover Ruby", + ["flowerruby"] = "Flower Ruby", + ["ribbonruby"] = "Ribbon Ruby", + ["strawberrymatcha"] = "Strawberry Matcha", + ["blueberrymatcha"] = "Blueberry Matcha", + ["lovematcha"] = "Love Matcha", + ["starmatcha"] = "Star Matcha", + ["clovermatcha"] = "Clover Matcha", + ["flowermatcha"] = "Flower Matcha", + ["ribbonmatcha"] = "Ribbon Matcha", + ["strawberrymint"] = "Strawberry Mint", + ["blueberrymint"] = "Blueberry Mint", + ["lovemint"] = "Love Mint", + ["starmint"] = "Star Mint", + ["clovermint"] = "Clover Mint", + ["flowermint"] = "Flower Mint", + ["ribbonmint"] = "Ribbon Mint", + ["strawberrylemon"] = "Strawberry Lemon", + ["blueberrylemon"] = "Blueberry Lemon", + ["lovelemon"] = "Love Lemon", + ["starlemon"] = "Star Lemon", + ["cloverlemon"] = "Clover Lemon", + ["flowerlemon"] = "Flower Lemon", + ["ribbonlemon"] = "Ribbon Lemon", + ["strawberrysalted"] = "Strawberry Salted", + ["blueberrysalted"] = "Blueberry Salted", + ["lovesalted"] = "Love Salted", + ["starsalted"] = "Star Salted", + ["cloversalted"] = "Clover Salted", + ["flowersalted"] = "Flower Salted", + ["ribbonsalted"] = "Ribbon Salted", + ["strawberryruby"] = "Strawberry Ruby", + ["blueberryruby"] = "Blueberry Ruby", + ["loveruby"] = "Love Ruby", + ["starruby"] = "Star Ruby", + ["cloverruby"] = "Clover Ruby", + ["flowerruby"] = "Flower Ruby", + ["ribbonruby"] = "Ribbon Ruby", + ["strawberrycaramel"] = "Strawberry Caramel", + ["blueberrycaramel"] = "Blueberry Caramel", + ["lovecaramel"] = "Love Caramel", + ["starcaramel"] = "Star Caramel", + ["clovercaramel"] = "Clover Caramel", + ["flowercaramel"] = "Flower Caramel", + ["ribboncaramel"] = "Ribbon Caramel", + ["strawberryrainbow"] = "Strawberry Rainbow", + ["blueberryrainbow"] = "Blueberry Rainbow", + ["loverainbow"] = "Love Rainbow", + ["starrainbow"] = "Star Rainbow", + ["cloverrainbow"] = "Clover Rainbow", + ["flowerrainbow"] = "Flower Rainbow", + ["ribbonrainbow"] = "Ribbon Rainbow", + ["iceface"] = "Ice Face", //eiscue + ["noiceface"] = "Noice Face", + ["fullbelly"] = "Full Belly", //morepko + ["full"] = "Full Belly", + ["hangry"] = "Hangry", + ["eternamax"] = "Eternamax", //eternatus + ["singlestrike"] = "Single Strike", //urshifu + ["single"] = "Single Strike", + ["rapidstrike"] = "Rapid Strike", + ["rapid"] = "Rapid Strike", + ["dada"] = "Dada", //zarude + ["icerider"] = "Ice Rider", //calyrex + ["shadowrider"] = "Shadow Rider", + ["overdrive"] = "Overdrive", //reshiram, zerkom, kyurem + ["radientsun"] = "Sun", //solgaleo + ["fullmoon"] = "Moon", //lunala + ["moon"] = "Moon", + ["zenith"] = "Zenith", //marshadow + #endregion + // compiled from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_with_form_differences on 11/9/21 + }; + public Task> Parse(IImmutableList args, Type[] genericTypes) + { + string toValidate = args[0].ToLower(); + ArgsParseResult result; + if (Forms.TryGetValue(toValidate, out string? formName)) + result = ArgsParseResult.Success(new Form(formName), args.Skip(1).ToImmutableList()); + else + result = ArgsParseResult.Failure("Invalid form name"); + return Task.FromResult(result); + } + } +} diff --git a/TPP.ArgsParsing/TypeParsers/ShinyParser.cs b/TPP.ArgsParsing/TypeParsers/ShinyParser.cs new file mode 100644 index 00000000..1bcef70c --- /dev/null +++ b/TPP.ArgsParsing/TypeParsers/ShinyParser.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using TPP.Common; +using TPP.ArgsParsing.Types; + +namespace TPP.ArgsParsing.TypeParsers +{ + /// + /// A parser that determines if something is indicated to be shiny or not. + /// + public class ShinyParser : IArgumentParser + { + string[] shinyWords = + { + "shiny", + "shiny:true" + }; + string[] plainWords = + { + "plain", + "regular", + "shiny:false" + }; + public Task> Parse(IImmutableList args, Type[] genericTypes) + { + string s = args[0].ToLower(); + ArgsParseResult result; + if (shinyWords.Contains(s)) + { + result = ArgsParseResult.Success(new Shiny { Value = true }, args.Skip(1).ToImmutableList()); + } + else if (plainWords.Contains(s)) + { + result = ArgsParseResult.Success(new Shiny { Value = false }, args.Skip(1).ToImmutableList()); + } + else + { + result = ArgsParseResult.Failure("The argument couldn't be understood as shiny or not", ErrorRelevanceConfidence.Unlikely); + } + return Task.FromResult(result); + } + } +} diff --git a/TPP.ArgsParsing/Types/AnyOrder.cs b/TPP.ArgsParsing/Types/AnyOrder.cs index b0c3cb08..7f4c8a89 100644 --- a/TPP.ArgsParsing/Types/AnyOrder.cs +++ b/TPP.ArgsParsing/Types/AnyOrder.cs @@ -67,4 +67,47 @@ public AnyOrder(T1 item1, T2 item2, T3 item3, T4 item4) public void Deconstruct(out T1 item1, out T2 item2, out T3 item3, out T4 item4) => (item1, item2, item3, item4) = (Item1, Item2, Item3, Item4); } + + public class AnyOrder : AnyOrder + { + public T1 Item1 { get; } + public T2 Item2 { get; } + public T3 Item3 { get; } + public T4 Item4 { get; } + public T5 Item5 { get; } + + public AnyOrder(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5) + { + Item1 = item1; + Item2 = item2; + Item3 = item3; + Item4 = item4; + Item5 = item5; + } + + public void Deconstruct(out T1 item1, out T2 item2, out T3 item3, out T4 item4, out T5 item5) => + (item1, item2, item3, item4, item5) = (Item1, Item2, Item3, Item4, Item5); + } + public class AnyOrder : AnyOrder + { + public T1 Item1 { get; } + public T2 Item2 { get; } + public T3 Item3 { get; } + public T4 Item4 { get; } + public T5 Item5 { get; } + public T6 Item6 { get; } + + public AnyOrder(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6) + { + Item1 = item1; + Item2 = item2; + Item3 = item3; + Item4 = item4; + Item5 = item5; + Item6 = item6; + } + + public void Deconstruct(out T1 item1, out T2 item2, out T3 item3, out T4 item4, out T5 item5, out T6 item6) => + (item1, item2, item3, item4, item5, item6) = (Item1, Item2, Item3, Item4, Item5, Item6); + } } diff --git a/TPP.ArgsParsing/Types/ImplicitBoolean.cs b/TPP.ArgsParsing/Types/ImplicitBoolean.cs new file mode 100644 index 00000000..6f2950d5 --- /dev/null +++ b/TPP.ArgsParsing/Types/ImplicitBoolean.cs @@ -0,0 +1,13 @@ +namespace TPP.ArgsParsing.Types +{ + public class ImplicitBoolean + { + public bool Value { get; internal init; } + public static implicit operator bool(ImplicitBoolean b) => b.Value; + public override string ToString() => Value.ToString(); + } + + public class Shiny : ImplicitBoolean + { + } +} diff --git a/TPP.ArgsParsing/Types/ImplicitString.cs b/TPP.ArgsParsing/Types/ImplicitString.cs new file mode 100644 index 00000000..f54ef822 --- /dev/null +++ b/TPP.ArgsParsing/Types/ImplicitString.cs @@ -0,0 +1,15 @@ +namespace TPP.ArgsParsing.Types +{ + public class ImplicitString + { + public string Name { get; internal init; } + public static implicit operator string(ImplicitString f) => f.Name; + public override string ToString() => Name.ToString(); + public ImplicitString(string s) => Name = s; + } + + public class Form : ImplicitString + { + public Form(string s) : base(s) => Name = s; + } +} diff --git a/TPP.Core/Commands/Definitions/BadgeCommands.cs b/TPP.Core/Commands/Definitions/BadgeCommands.cs index e9d050d4..695d6063 100644 --- a/TPP.Core/Commands/Definitions/BadgeCommands.cs +++ b/TPP.Core/Commands/Definitions/BadgeCommands.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -30,7 +31,7 @@ public class BadgeCommands : ICommandCollection new Command("badges", Badges) { Aliases = new[] { "badge" }, - Description = "Show a user's badges. Argument: (optional) (optional)" + Description = "Show a user's badges. Arguments: (optional) (optional)" }, new Command("unselectbadge", UnselectBadge) @@ -54,12 +55,18 @@ public class BadgeCommands : ICommandCollection new Command("giftbadge", GiftBadge) { Description = - "Gift a badge you own to another user with no price. Arguments: (Optional) " + "Gift a badge you own to another user with no price. Arguments: (Optional) (optional) (optional) (optional)" }, + new Command("listsellbadge", ListSellBadge) + { + Description = + "List the badges someone is selling. Arguments: (optional) (optional) (optional) (optional) (optional)" + } }; private readonly IBadgeRepo _badgeRepo; private readonly IUserRepo _userRepo; + private readonly IBadgeMarketRepo _badgeMarketRepo; private readonly IMessageSender _messageSender; private readonly HashSet? _whitelist; private readonly IImmutableSet _knownSpecies; @@ -68,6 +75,7 @@ public class BadgeCommands : ICommandCollection public BadgeCommands( IBadgeRepo badgeRepo, IUserRepo userRepo, + IBadgeMarketRepo badgeMarketRepo, IMessageSender messageSender, IImmutableSet knownSpecies, HashSet? whitelist = null @@ -75,6 +83,7 @@ public BadgeCommands( { _badgeRepo = badgeRepo; _userRepo = userRepo; + _badgeMarketRepo = badgeMarketRepo; _messageSender = messageSender; _knownSpecies = knownSpecies; _whitelist = whitelist; @@ -93,47 +102,86 @@ public BadgeCommands( }; } + /// + /// Convineintly handles many optional arguments for badge commands. May throw an exception when given faulty form informaton, so this should be surrounded by a try/catch + /// + private static (Badge.BadgeSource?, string?, bool) InterpretBadgeInfoArgs(Optional sourceOpt, Optional formOpt, Optional shinyOpt) + { + Badge.BadgeSource? source = sourceOpt.IsPresent ? sourceOpt.Value : null; + string? form = formOpt.IsPresent ? formOpt.Value.Name : null; + bool shiny = shinyOpt.IsPresent ? shinyOpt.Value : false; + return (source, form, shiny); + } + private static string describeBadge(PkmnSpecies? species, Badge.BadgeSource? source, string? form, bool shiny) + { + string speciesStr = species != null ? species.ToString() : ""; + string formStr = form != null ? form + " " : ""; + string shinyStr = shiny ? "shiny " : ""; + string sourceStr = ""; + switch (source) + { + case Badge.BadgeSource.Pinball: + sourceStr = "pinball caught "; + break; + case Badge.BadgeSource.RunCaught: + sourceStr = "run caught "; + break; + case Badge.BadgeSource.ManualDistribution: + sourceStr = "manually distributed "; + break; + case Badge.BadgeSource.ManualCreation: + sourceStr = "admin created "; + break; + case Badge.BadgeSource.Crate: + sourceStr = "crate dropped "; + break; + case Badge.BadgeSource.Breaking: + sourceStr = "badge breaker dropped "; + break; + case Badge.BadgeSource.Transmutation: + sourceStr = "transmuted "; + break; + default: + break; + } + return $"{sourceStr}{shinyStr}{formStr}{speciesStr}".TrimEnd(); + } + public async Task Badges(CommandContext context) { - (Optional optionalSpecies, Optional optionalUser) = - await context.ParseArgs, Optional>>(); + (Optional optionalSpecies, Optional optionalUser, Optional sourceOpt, Optional formOpt, Optional shinyOpt) = + await context.ParseArgs, Optional, Optional, Optional, Optional>>(); bool isSelf = !optionalUser.IsPresent; User user = isSelf ? context.Message.User : optionalUser.Value; - if (optionalSpecies.IsPresent) + PkmnSpecies? species = optionalSpecies.IsPresent ? optionalSpecies.Value : null; + (Badge.BadgeSource? source, string? form, bool? shiny) = InterpretBadgeInfoArgs(sourceOpt, formOpt, shinyOpt); + + List badges = await _badgeRepo.FindAllByCustom(user.Id, species, form, source, shiny); + if (!badges.Any()) { - PkmnSpecies species = optionalSpecies.Value; - long numBadges = await _badgeRepo.CountByUserAndSpecies(user.Id, species); return new CommandResult { - Response = numBadges == 0 - ? isSelf - ? $"You have no {species} badges." - : $"{user.Name} has no {species} badges." - : isSelf - ? $"You have {numBadges}x {species} badges." - : $"{user.Name} has {numBadges}x {species} badges." + Response = isSelf ? "You have no badges." : $"{user.Name} has no badges." }; } - else + badges = badges.OrderBy(b => b.Species).ThenBy(b => b.Shiny).ThenBy(b => b.Source).ThenBy(b => b.Form).ToList(); + Dictionary countPerMetadata = new Dictionary(); + foreach (Badge b in badges) { - ImmutableSortedDictionary numBadgesPerSpecies = - await _badgeRepo.CountByUserPerSpecies(user.Id); - if (!numBadgesPerSpecies.Any()) - { - return new CommandResult - { - Response = isSelf ? "You have no badges." : $"{user.Name} has no badges." - }; - } - IEnumerable badgesFormatted = numBadgesPerSpecies.Select(kvp => $"{kvp.Value}x {kvp.Key}"); - return new CommandResult - { - Response = isSelf - ? $"Your badges: {string.Join(", ", badgesFormatted)}" - : $"{user.Name}'s badges: {string.Join(", ", badgesFormatted)}", - ResponseTarget = ResponseTarget.WhisperIfLong - }; + string metadadaAsString = describeBadge(b.Species, b.Source, b.Form, b.Shiny); + if (countPerMetadata.Keys.Contains(metadadaAsString)) + countPerMetadata[metadadaAsString] += 1; + else + countPerMetadata.Add(metadadaAsString, 1); } + IEnumerable badgesFormatted = countPerMetadata.Select(kvp => $"{kvp.Value}x {kvp.Key}"); + return new CommandResult + { + Response = isSelf + ? $"Your badges: {string.Join(", ", badgesFormatted)}" + : $"{user.Name}'s badges: {string.Join(", ", badgesFormatted)}", + ResponseTarget = ResponseTarget.WhisperIfLong + }; } public async Task UnselectBadge(CommandContext context) @@ -310,18 +358,20 @@ public async Task Pokedex(CommandContext context) public async Task GiftBadge(CommandContext context) { User gifter = context.Message.User; - (User recipient, PkmnSpecies species, Optional amountOpt) = - await context.ParseArgs>>(); + (User recipient, PkmnSpecies species, Optional amountOpt, Optional sourceOpt, Optional shinyOpt, Optional formOpt) = + await context.ParseArgs, Optional, Optional, Optional>>(); int amount = amountOpt.Map(i => i.Number).OrElse(1); + (Badge.BadgeSource? source, string? form, bool shiny) = InterpretBadgeInfoArgs(sourceOpt, formOpt, shinyOpt); if (recipient == gifter) return new CommandResult { Response = "You cannot gift to yourself" }; - List badges = await _badgeRepo.FindByUserAndSpecies(gifter.Id, species, amount); + List badges = await _badgeRepo.FindAllByCustom(gifter.Id, species, form, source, shiny); if (badges.Count < amount) + //TODO big improve before merge return new CommandResult { - Response = $"You tried to gift {amount} {species} badges, but you only have {badges.Count}." + Response = $"You tried to gift {amount} {describeBadge(species, source, form, shiny)} badges, but you only have {badges.Count}." }; IImmutableList badgesToGift = badges.Take(amount).ToImmutableList(); @@ -329,15 +379,56 @@ public async Task GiftBadge(CommandContext context) await _badgeRepo.TransferBadges(badgesToGift, recipient.Id, BadgeLogType.TransferGift, data); await _messageSender.SendWhisper(recipient, amount > 1 - ? $"You have been gifted {amount} {species} badges from {gifter.Name}!" - : $"You have been gifted a {species} badge from {gifter.Name}!"); + ? $"You have been gifted {amount} {describeBadge(species, source, form, shiny)} badges from {gifter.Name}!" + : $"You have been gifted a {describeBadge(species, source, form, shiny)} badge from {gifter.Name}!"); return new CommandResult { Response = amount > 1 - ? $"has gifted {amount} {species} badges to {recipient.Name}!" - : $"has gifted a {species} badge to {recipient.Name}!", + ? $"has gifted {amount} {describeBadge(species, source, form, shiny)} badges to {recipient.Name}!" + : $"has gifted a {describeBadge(species, source, form, shiny)} badge to {recipient.Name}!", ResponseTarget = ResponseTarget.Chat }; } + + public async Task ListSellBadge(CommandContext context) + { + (Optional userOpt, Optional speciesOpt, Optional sourceOpt, Optional shinyOpt, Optional formOpt) = + await context.ParseArgs, Optional, Optional, Optional, Optional>>(); + + User user = userOpt.IsPresent ? userOpt.Value : context.Message.User; + PkmnSpecies? species = speciesOpt.IsPresent ? speciesOpt.Value : null; + (Badge.BadgeSource? source, string? form, bool? shiny) = InterpretBadgeInfoArgs(sourceOpt, formOpt, shinyOpt); + + List forSale = await _badgeMarketRepo.FindAllBadgesForSale(user.Id, species, form, source, shiny); + + string response; + if (forSale.Count == 0) + response = "No badges found."; + else + { + Dictionary countPerMetadata = new Dictionary(); + foreach (Badge b in forSale) + { + if (b.UserId == null) + throw new OwnedBadgeNotFoundException(b); + User? seller = await _userRepo.FindById(b.UserId); + if (seller == null) + throw new OwnedBadgeNotFoundException(b); + + string metadadaAsString = describeBadge(b.Species, b.Source, b.Form, b.Shiny) + $" sold by {seller.SimpleName} for T{b.SellPrice}"; + if (countPerMetadata.Keys.Contains(metadadaAsString)) + countPerMetadata[metadadaAsString] += 1; + else + countPerMetadata.Add(metadadaAsString, 1); + } + IEnumerable badgesFormatted = countPerMetadata.Select(kvp => $"{kvp.Value}x {kvp.Key}"); + response = $"{forSale.Count} badges found: " + string.Join(", ", badgesFormatted); + } + + return new CommandResult + { + Response = response, + }; + } } } diff --git a/TPP.Core/Commands/Definitions/OperatorCommands.cs b/TPP.Core/Commands/Definitions/OperatorCommands.cs index e8c04bdb..378aedee 100644 --- a/TPP.Core/Commands/Definitions/OperatorCommands.cs +++ b/TPP.Core/Commands/Definitions/OperatorCommands.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -197,18 +198,21 @@ await _messageSender.SendWhisper(recipient, amount > 1 public async Task CreateBadge(CommandContext context) { - (User recipient, PkmnSpecies species, Optional amountOpt) = - await context.ParseArgs>>(); + (User recipient, PkmnSpecies species, Optional amountOpt, Optional formOpt, Optional shinyOpt) = + await context.ParseArgs, Optional, Optional>>(); int amount = amountOpt.Map(i => i.Number).OrElse(1); + string? form = formOpt.IsPresent ? formOpt.Value.Name : null; + bool shiny = shinyOpt.IsPresent ? shinyOpt.Value : false; for (int i = 0; i < amount; i++) - await _badgeRepo.AddBadge(recipient.Id, species, Badge.BadgeSource.ManualCreation); + await _badgeRepo.AddBadge(recipient.Id, species, Badge.BadgeSource.ManualCreation, form, shiny); + form = form == null ? "" : form + " "; return new CommandResult { Response = amount > 1 - ? $"{amount} badges of species {species} created for user {recipient.Name}." - : $"Badge of species {species} created for user {recipient.Name}." + ? $"{amount} {form}{species} badges created for {recipient.Name}." + : $"{form}{species} badge created for {recipient.Name}." }; } diff --git a/TPP.Core/Setups.cs b/TPP.Core/Setups.cs index 780d3e62..799ac6c6 100644 --- a/TPP.Core/Setups.cs +++ b/TPP.Core/Setups.cs @@ -42,6 +42,9 @@ public static ArgsParser SetUpArgsParser(IUserRepo userRepo, PokedexData pokedex argsParser.AddArgumentParser(new TokensParser()); argsParser.AddArgumentParser(new SignedPokeyenParser()); argsParser.AddArgumentParser(new SignedTokensParser()); + argsParser.AddArgumentParser(new ShinyParser()); + argsParser.AddArgumentParser(new BadgeSourceParser()); + argsParser.AddArgumentParser(new FormParser()); argsParser.AddArgumentParser(new RoleParser()); argsParser.AddArgumentParser(new PercentageParser()); argsParser.AddArgumentParser(new SideParser()); @@ -83,7 +86,7 @@ public static CommandProcessor SetUpCommandProcessor( ).Commands, new PollCommands(databases.PollRepo).Commands, new ManagePollCommands(databases.PollRepo).Commands, - new BadgeCommands(databases.BadgeRepo, databases.UserRepo, messageSender, knownSpecies).Commands, + new BadgeCommands(databases.BadgeRepo, databases.UserRepo, databases.BadgeMarketRepo, messageSender, knownSpecies).Commands, new OperatorCommands( stopToken, databases.PokeyenBank, databases.TokensBank, messageSender: messageSender, databases.BadgeRepo, databases.UserRepo @@ -116,7 +119,8 @@ public record Databases( ISubscriptionLogRepo SubscriptionLogRepo, IModLogRepo ModLogRepo, IResponseCommandRepo ResponseCommandRepo, - KeyValueStore KeyValueStore + KeyValueStore KeyValueStore, + IBadgeMarketRepo BadgeMarketRepo ); public static Databases SetUpRepositories(ILogger logger, BaseConfig baseConfig) @@ -151,6 +155,7 @@ public static Databases SetUpRepositories(ILogger logger, BaseConfig baseConfig) clock: clock); tokenBank.AddReservedMoneyChecker( new PersistedReservedMoneyCheckers(mongoDatabase).AllDatabaseReservedTokens); + IBadgeMarketRepo badgeMarketRepo = new BadgeMarketRepo(mongoDatabase, badgeRepo, userRepo, tokenBank, clock); return new Databases ( UserRepo: userRepo, @@ -165,7 +170,8 @@ public static Databases SetUpRepositories(ILogger logger, BaseConfig baseConfig) SubscriptionLogRepo: new SubscriptionLogRepo(mongoDatabase), ModLogRepo: new ModLogRepo(mongoDatabase), ResponseCommandRepo: new ResponseCommandRepo(mongoDatabase), - KeyValueStore: new KeyValueStore(mongoDatabase) + KeyValueStore: new KeyValueStore(mongoDatabase), + BadgeMarketRepo: badgeMarketRepo ); } diff --git a/TPP.Model/Badge.cs b/TPP.Model/Badge.cs index 636a5eeb..c258eb66 100644 --- a/TPP.Model/Badge.cs +++ b/TPP.Model/Badge.cs @@ -1,5 +1,6 @@ using NodaTime; using TPP.Common; +using System.ComponentModel.DataAnnotations; namespace TPP.Model { @@ -43,6 +44,16 @@ public enum BadgeSource /// public Instant CreatedAt { get; init; } + /// + /// If this pokemon has multiple forms, which form it is. + /// + public string? Form { get; init; } + + /// + /// If this badge is shiny. + /// + public bool Shiny { get; init; } + /// If this badge is on sale, for how much. public long? SellPrice { get; init; } /// If this badge is on sale, since when. @@ -53,13 +64,17 @@ public Badge( string? userId, PkmnSpecies species, BadgeSource source, - Instant createdAt) + Instant createdAt, + string? form, + bool shiny) { Id = id; UserId = userId; Species = species; Source = source; CreatedAt = createdAt; + Form = form; + Shiny = shiny; } public override string ToString() => $"Badge({Species}@{UserId ?? ""})"; diff --git a/TPP.Model/BadgeBuyOffer.cs b/TPP.Model/BadgeBuyOffer.cs new file mode 100644 index 00000000..f1dbc6a3 --- /dev/null +++ b/TPP.Model/BadgeBuyOffer.cs @@ -0,0 +1,90 @@ +using NodaTime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TPP.Common; + +namespace TPP.Model +{ + public class BadgeBuyOffer + { + /// + /// Unique Id. + /// + public string Id { get; init; } + + /// + /// The ID of the user that created the buy offer. + /// + public string UserId { get; init; } + + /// + /// The species of pokemon to buy. + /// + public PkmnSpecies Species { get; init; } + + /// + /// The form of pokemon to buy. + /// + public string? Form { get; init; } + + /// + /// The source of the badge to buy. + /// + public Badge.BadgeSource? Source { get; init; } + + /// + /// Is the offer seeking shiny badges. + /// + public bool? Shiny { get; init; } + + /// + /// How much to pay for each badge. + /// + public int Price { get; init; } + + /// + /// The number of badges to buy. + /// + public int Amount { get; private set; } + + /// + /// When the buy offer was created. + /// + public Instant CreatedAt { get; init; } + + /// + /// When this offer was last updated. + /// + public Instant WaitingSince { get; private set; } + + //duration and expires_at depricated from old core + + public BadgeBuyOffer(string id, string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int price, int amount, Instant createdAt) + { + Id = id; + UserId = userId; + Species = species; + Form = form; + Source = source; + Shiny = shiny; + Price = price; + Amount = amount; + CreatedAt = createdAt; + WaitingSince = createdAt; + } + + /// + /// Decrement the amount to buy. + /// + public void decrement(Instant decrementedAt) + { + if (Amount <= 0) + throw new InvalidOperationException("The buy offer has no badges remaining, and cannot be decremented further."); + Amount--; + WaitingSince = decrementedAt; + } + } +} diff --git a/TPP.Persistence.MongoDB/Repos/BadgeMarketRepo.cs b/TPP.Persistence.MongoDB/Repos/BadgeMarketRepo.cs new file mode 100644 index 00000000..d8f8df89 --- /dev/null +++ b/TPP.Persistence.MongoDB/Repos/BadgeMarketRepo.cs @@ -0,0 +1,249 @@ +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.IdGenerators; +using MongoDB.Driver; +using NodaTime; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Collections.Immutable; +using System.Threading.Tasks; +using TPP.Common; +using TPP.Model; +using TPP.Persistence.MongoDB.Serializers; +using TPP.Persistence; + +namespace TPP.Persistence.MongoDB.Repos +{ + public class BadgeMarketRepo : IBadgeMarketRepo + { + private const string BuyOfferCollectionName = "badgebuyoffers"; + public readonly IMongoCollection BuyOfferCollection; + private readonly IBadgeRepo _badgeRepo; + private readonly IClock _clock; + private readonly IUserRepo _userRepo; + private readonly IBank _tokenBank; + + static BadgeMarketRepo() + { + BsonClassMap.RegisterClassMap(cm => + { + cm.MapIdProperty(o => o.Id) + .SetIdGenerator(StringObjectIdGenerator.Instance) + .SetSerializer(ObjectIdAsStringSerializer.Instance); + cm.MapProperty(o => o.UserId).SetElementName("user"); + cm.MapProperty(o => o.Species).SetElementName("species"); + cm.MapProperty(o => o.Source).SetElementName("source"); + cm.MapProperty(o => o.CreatedAt).SetElementName("created_at"); + cm.MapProperty(o => o.Price).SetElementName("price"); + cm.MapProperty(o => o.Amount).SetElementName("amount"); + cm.MapProperty(o => o.Form).SetElementName("form") + .SetIgnoreIfNull(true); + cm.MapProperty(o => o.Shiny).SetElementName("shiny") + .SetDefaultValue(false) + .SetIgnoreIfDefault(true); + }); + } + + private void InitIndexes() + { + BuyOfferCollection.Indexes.CreateMany(new[] + { + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.UserId)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Species)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Source)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.CreatedAt)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Form)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Shiny)), + }); + } + + public BadgeMarketRepo(IMongoDatabase database, IBadgeRepo badgeRepo, IUserRepo userRepo, IBank bank, IClock clock) + { + _badgeRepo = badgeRepo; + _userRepo = userRepo; + _tokenBank = bank; + database.CreateCollectionIfNotExists(BuyOfferCollectionName).Wait(); + BuyOfferCollection = database.GetCollection(BuyOfferCollectionName); + _clock = clock; + InitIndexes(); + } + + public async Task> FindAllBuyOffers(string? userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny) + { + FilterDefinition filter = Builders.Filter.Empty; + if (userId != null) + filter &= Builders.Filter.Eq(b => b.UserId, userId); + if (species != null) + filter &= Builders.Filter.Eq(b => b.Species, species); + if (form != null) + filter &= Builders.Filter.Eq(b => b.Form, form); + if (source != null) + filter &= Builders.Filter.Eq(b => b.Source, source); + if (shiny == true) + filter &= Builders.Filter.Eq(b => b.Shiny, true); + else + filter &= Builders.Filter.Ne(b => b.Shiny, true); + + return await BuyOfferCollection.Find(filter).ToListAsync(); + } + + public async Task> FindAllBadgesForSale(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny) + { + return await _badgeRepo.FindAllForSaleByCustom(userId, species, form, source, shiny); + } + + public async Task CreateBuyOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int price, int amount, Instant? createdAt = null) + { + BadgeBuyOffer buyOffer = new BadgeBuyOffer( + id: string.Empty, + userId: userId, + species: species, + form: form, + source: source, + shiny: shiny, + price: price, + amount: amount, + createdAt: createdAt ?? _clock.GetCurrentInstant() + ); + + await BuyOfferCollection.InsertOneAsync(buyOffer); + Debug.Assert(buyOffer.Id.Length > 0, "The MongoDB driver injected a generated ID"); + return buyOffer; + } + + public async Task CreateSellOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int price) + { + List notSellingOwnedByUser = await _badgeRepo.FindAllNotForSaleByCustom(userId, species, form, source, shiny); + Badge toSell = SortBySpecialness(notSellingOwnedByUser).First(); + Badge selling = await _badgeRepo.SetBadgeSellPrice(toSell, price); + return selling; + } + + public async Task DeleteBuyOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int amount) + { + List offers = await FindAllBuyOffers(userId, species, form, source, shiny); + if (offers.Count() < amount) + throw new ArgumentException(string.Format("Tried to cancel {0} offers but only {1} were found", amount, offers.Count)); + + offers = offers.OrderByDescending(o => o.CreatedAt).ToList(); + for (int i = 0; i < amount; i++) + { + await BuyOfferCollection.FindOneAndDeleteAsync(o => o.Id == offers[i].Id); + } + } + public async Task DeleteSellOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int amount) + { + List badgesForSale = await FindAllBadgesForSale(userId, species, form, source, shiny); + if (amount > badgesForSale.Count) + throw new ArgumentException(string.Format("Tried to cancel {0} offers but only {1} were found", amount, badgesForSale.Count)); + + badgesForSale = SortBySpecialness(badgesForSale); + badgesForSale.Reverse(); + for (int i = 0; i < amount; i++) + { + await _badgeRepo.SetBadgeSellPrice(badgesForSale[i], 0); + } + } + + public async Task> ResolveBuyOffers(PkmnSpecies species, bool? shiny) + { + List badgesForSale = await FindAllBadgesForSale(null, species, null, null, shiny); + badgesForSale = badgesForSale.OrderByDescending(b => b.SellPrice).ThenBy(b => b.SellingSince).ToList(); + List soldBadges = new List(); + + foreach (Badge badge in badgesForSale) + { + List buyOffers = await FindAllBuyOffers(null, species, null, null, shiny); + buyOffers = buyOffers.Where(o => o.Price >= badge.SellPrice).ToList(); + buyOffers = buyOffers.OrderBy(o => o.WaitingSince).ToList(); + + if (badge.SellPrice == null) + throw new InvalidOperationException("Tried to sell a badge with no sell price"); + + foreach (BadgeBuyOffer offer in buyOffers) + { + if (offer.UserId == badge.UserId) + continue; //user shouldn't sell to themself + if (badge.UserId == null) + throw new OwnedBadgeNotFoundException(badge); + if (((offer.Form != null) && (offer.Form != badge.Form)) + || ((offer.Source != null) && (offer.Source != badge.Source))) + continue; + + User? buyer = await _userRepo.FindById(offer.UserId); + User? seller = await _userRepo.FindById(badge.UserId); + if (buyer == null) + throw new UserNotFoundException(offer.UserId); + if (seller == null) + throw new UserNotFoundException(badge.UserId); + + soldBadges.Add(new IBadgeMarketRepo.BadgeSale(seller, buyer, badge, (long)badge.SellPrice)); + await _tokenBank.PerformTransactions( + new Transaction[] + { + new Transaction(buyer, -1 * offer.Price, "BadgePurchase"), + new Transaction(seller, offer.Price, "BadgeSale") + } + ); + await _badgeRepo.TransferBadges(new List { badge }.ToImmutableList(), buyer.Id, "BadgeSale", new Dictionary() { }); + await ResetUserSellOffers(badge.UserId, badge.Species, badge.Form, badge.Shiny); + offer.decrement(_clock.GetCurrentInstant()); + if (offer.Amount > 0) + await BuyOfferCollection.FindOneAndReplaceAsync(o => o.Id == offer.Id, offer); + else + await BuyOfferCollection.FindOneAndDeleteAsync(o => o.Id == offer.Id); + break; //this badge has been sold, ignore the rest of the offers + } + } + return soldBadges.ToImmutableList(); + } + /// + /// Sorts badges according to the priority in which they should be sold. + /// Current sorting rule: prioritize keeping 1 of each form, then sell newer badges first. Top priority badge will be selected for sale when fufilling offers. + /// + private static List SortBySpecialness(IEnumerable toSort) + { + IEnumerable duplicates; + IEnumerable uniques = new List(); + HashSet formNames = new HashSet(); + foreach (Badge b in toSort) + { + formNames.Add(b.Form); + } + + foreach (string? form in formNames) + { + IEnumerable ofSingleForm = toSort.Where(b => b.Form == form); + ofSingleForm = ofSingleForm.OrderByDescending(b => b.CreatedAt); + uniques = uniques.Append(ofSingleForm.Last()); + } + + duplicates = toSort.Except(uniques); + duplicates = duplicates.OrderByDescending(b => b.CreatedAt); + uniques = uniques.OrderByDescending(b => b.CreatedAt); + + IEnumerable result = duplicates; + foreach (Badge b in uniques) + { + result = result.Append(b); + } + return result.ToList(); + } + + /// + /// Refresh user sell offers of a particular species and form. Moves them to the back of the line to fufill orders. + /// + private async Task ResetUserSellOffers(string userId, PkmnSpecies species, string? form, bool shiny) + { + List forSale = await FindAllBadgesForSale(userId, species, form, null, shiny); + foreach (Badge b in forSale) + { + if (b.SellPrice == null) + throw new OwnedBadgeNotFoundException(b); + await _badgeRepo.SetBadgeSellPrice(b, (long)b.SellPrice); + } + } + } +} diff --git a/TPP.Persistence.MongoDB/Repos/BadgeRepo.cs b/TPP.Persistence.MongoDB/Repos/BadgeRepo.cs index 6501659b..58144d07 100644 --- a/TPP.Persistence.MongoDB/Repos/BadgeRepo.cs +++ b/TPP.Persistence.MongoDB/Repos/BadgeRepo.cs @@ -54,10 +54,14 @@ static BadgeRepo() cm.MapProperty(b => b.Species).SetElementName("species"); cm.MapProperty(b => b.Source).SetElementName("source"); cm.MapProperty(b => b.CreatedAt).SetElementName("created_at"); + cm.MapProperty(b => b.Form).SetElementName("form"); cm.MapProperty(b => b.SellPrice).SetElementName("sell_price") .SetIgnoreIfNull(true); cm.MapProperty(b => b.SellingSince).SetElementName("selling_since") .SetIgnoreIfNull(true); + cm.MapProperty(b => b.Shiny).SetElementName("shiny") + .SetDefaultValue(false) + .SetIgnoreIfDefault(true); }); BsonClassMap.RegisterClassMap(cm => { @@ -94,19 +98,25 @@ private void InitIndexes() { new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.UserId)), new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Species)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Source)), + // TODO really ascending...?: new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.CreatedAt)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Form)), + new CreateIndexModel(Builders.IndexKeys.Ascending(u => u.Shiny)), }); } public async Task AddBadge( - string? userId, PkmnSpecies species, Badge.BadgeSource source, Instant? createdAt = null) + string? userId, PkmnSpecies species, Badge.BadgeSource source, string? form, bool shiny, Instant? createdAt = null) { var badge = new Badge( id: string.Empty, userId: userId, species: species, source: source, - createdAt: createdAt ?? _clock.GetCurrentInstant() + createdAt: createdAt ?? _clock.GetCurrentInstant(), + form: form, + shiny: shiny ); await Collection.InsertOneAsync(badge); Debug.Assert(badge.Id.Length > 0, "The MongoDB driver injected a generated ID"); @@ -114,12 +124,46 @@ public async Task AddBadge( return badge; } - public async Task> FindByUser(string? userId) => + public async Task> FindAllByUser(string? userId) => await Collection.Find(b => b.UserId == userId).ToListAsync(); public async Task> FindByUserAndSpecies(string? userId, PkmnSpecies species, int? limit = null) => await Collection.Find(b => b.UserId == userId && b.Species == species).Limit(limit).ToListAsync(); + public async Task> FindAllByCustom(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny) + { + FilterDefinition filter = Builders.Filter.Empty; + if (userId != null) + filter &= Builders.Filter.Eq(b => b.UserId, userId); + else + filter &= Builders.Filter.Ne(b => b.UserId, null); + if (species != null) + filter &= Builders.Filter.Eq(b => b.Species, species); + if (form != null) + filter &= Builders.Filter.Eq(b => b.Form, form); + if (source != null) + filter &= Builders.Filter.Eq(b => b.Source, source); + if (shiny == true) + filter &= Builders.Filter.Eq(b => b.Shiny, true); + else + //match everything not true, including null/nonexistance. (querying for shiny==false misses these) + filter &= Builders.Filter.Ne(b => b.Shiny, true); + + return await Collection.Find(filter).ToListAsync(); + } + + public async Task> FindAllForSaleByCustom(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny) + { + List all = await FindAllByCustom(userId, species, form, source, shiny); + return all.Where(b => b.SellPrice > 0).ToList(); + } + + public async Task> FindAllNotForSaleByCustom(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny) + { + List all = await FindAllByCustom(userId, species, form, source, shiny); + return all.Where(b => b.SellPrice == 0).ToList(); + } + public async Task CountByUserAndSpecies(string? userId, PkmnSpecies species) => await Collection.CountDocumentsAsync(b => b.UserId == userId && b.Species == species); @@ -198,6 +242,23 @@ await _badgeLogRepo.LogWithSession( } return updatedBadges.ToImmutableList(); } + public async Task SetBadgeSellPrice(Badge badge, long price) + { + if (price <= 0) + throw new ArgumentOutOfRangeException("price", "Price must be positive"); + + long? sellPrice = price == 0 ? null : price; + Instant? sellingSince = sellPrice == null ? null : _clock.GetCurrentInstant(); + + return await Collection.FindOneAndUpdateAsync( + Builders.Filter + .Where(b => b.Id == badge.Id && b.UserId == badge.UserId), + Builders.Update + .Set(b => b.SellPrice, sellPrice) + .Set(b => b.SellingSince, sellingSince), + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After, IsUpsert = false } + ) ?? throw new OwnedBadgeNotFoundException(badge); + } public async Task RenewBadgeStats(IImmutableSet? onlyTheseSpecies = null) { diff --git a/TPP.Persistence.MongoDB/Repos/UserRepo.cs b/TPP.Persistence.MongoDB/Repos/UserRepo.cs index aac38647..a0bc7812 100644 --- a/TPP.Persistence.MongoDB/Repos/UserRepo.cs +++ b/TPP.Persistence.MongoDB/Repos/UserRepo.cs @@ -162,6 +162,8 @@ public async Task RecordUser(UserInfo userInfo) public async Task FindByDisplayName(string displayName) => await Collection.Find(u => u.TwitchDisplayName == displayName).FirstOrDefaultAsync(); + public async Task FindById(string userId) => + await Collection.Find(u => u.Id == userId).FirstOrDefaultAsync(); public async Task> FindAllByPokeyenUnder(long yen) => await Collection.Find(u => u.Pokeyen < yen).ToListAsync(); diff --git a/TPP.Persistence/IBadgeMarketRepo.cs b/TPP.Persistence/IBadgeMarketRepo.cs new file mode 100644 index 00000000..1897f84e --- /dev/null +++ b/TPP.Persistence/IBadgeMarketRepo.cs @@ -0,0 +1,24 @@ +using NodaTime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TPP.Common; +using TPP.Model; +using System.Collections.Immutable; + +namespace TPP.Persistence +{ + public interface IBadgeMarketRepo + { + record BadgeSale(User seller, User buyer, Badge soldBadge, long price); + Task> FindAllBuyOffers(string? userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny); + Task> FindAllBadgesForSale(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny); + Task CreateBuyOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int price, int amount, Instant? createdAt); + Task CreateSellOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int price); + Task DeleteBuyOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int amount); + Task DeleteSellOffer(string userId, PkmnSpecies species, string? form, Badge.BadgeSource? source, bool? shiny, int amount); + Task> ResolveBuyOffers(PkmnSpecies species, bool? shiny); + } +} diff --git a/TPP.Persistence/IBadgeRepo.cs b/TPP.Persistence/IBadgeRepo.cs index 64aeabfb..be26f905 100644 --- a/TPP.Persistence/IBadgeRepo.cs +++ b/TPP.Persistence/IBadgeRepo.cs @@ -44,9 +44,10 @@ public interface IBadgeStatsRepo public interface IBadgeRepo { public Task AddBadge( - string? userId, PkmnSpecies species, Badge.BadgeSource source, Instant? createdAt = null); - public Task> FindByUser(string? userId); - public Task> FindByUserAndSpecies(string? userId, PkmnSpecies species, int? limit = null); + string? userId, PkmnSpecies species, Badge.BadgeSource source, string? form, bool shiny, Instant? createdAt = null); + public Task> FindAllByUser(string? userId); + public Task> FindByUserAndSpecies(string? userId, PkmnSpecies species, int? limit); + public Task> FindAllByCustom(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny); public Task CountByUserAndSpecies(string? userId, PkmnSpecies species); public Task> CountByUserPerSpecies(string? userId); public Task HasUserBadge(string? userId, PkmnSpecies species); @@ -56,5 +57,10 @@ public Task AddBadge( public Task> TransferBadges( IImmutableList badges, string? recipientUserId, string reason, IDictionary additionalData); + + public Task SetBadgeSellPrice(Badge badge, long price); + public Task> FindAllForSaleByCustom(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny); + + public Task> FindAllNotForSaleByCustom(string? userId, PkmnSpecies? species, string? form, Badge.BadgeSource? source, bool? shiny); } } diff --git a/TPP.Persistence/IUserRepo.cs b/TPP.Persistence/IUserRepo.cs index 7312e7d0..5eb1adc6 100644 --- a/TPP.Persistence/IUserRepo.cs +++ b/TPP.Persistence/IUserRepo.cs @@ -11,6 +11,7 @@ public interface IUserRepo public Task RecordUser(UserInfo userInfo); public Task FindBySimpleName(string simpleName); public Task FindByDisplayName(string displayName); + public Task FindById(string userId); public Task> FindAllByPokeyenUnder(long yen); public Task> FindAllByRole(Role role); public Task SetSelectedBadge(User user, PkmnSpecies? badge); diff --git a/tests/TPP.Core.Tests/Commands/Definitions/BadgeCommandsTest.cs b/tests/TPP.Core.Tests/Commands/Definitions/BadgeCommandsTest.cs index 78ae8bef..3b18a188 100644 --- a/tests/TPP.Core.Tests/Commands/Definitions/BadgeCommandsTest.cs +++ b/tests/TPP.Core.Tests/Commands/Definitions/BadgeCommandsTest.cs @@ -31,6 +31,7 @@ private static Message MockMessage(User user, string text = "") => private Mock _badgeRepoMock = null!; private Mock _userRepoMock = null!; + private Mock _badgeMarketRepoMock = null!; private Mock _messageSender = null!; private ArgsParser _argsParser = null!; @@ -42,13 +43,17 @@ public void SetUp() _badgeRepoMock = new Mock(); _userRepoMock = new Mock(); _messageSender = new Mock(); - _badgeCommands = new BadgeCommands(_badgeRepoMock.Object, _userRepoMock.Object, _messageSender.Object, - ImmutableHashSet.Empty); + _badgeMarketRepoMock = new Mock(); + _badgeCommands = new BadgeCommands(_badgeRepoMock.Object, _userRepoMock.Object, _badgeMarketRepoMock.Object, + _messageSender.Object, ImmutableHashSet.Empty); _argsParser = new ArgsParser(); _argsParser.AddArgumentParser(new OptionalParser(_argsParser)); _argsParser.AddArgumentParser(new UserParser(_userRepoMock.Object)); _argsParser.AddArgumentParser(new AnyOrderParser(_argsParser)); _argsParser.AddArgumentParser(new PositiveIntParser()); + _argsParser.AddArgumentParser(new BadgeSourceParser()); + _argsParser.AddArgumentParser(new FormParser()); + _argsParser.AddArgumentParser(new ShinyParser()); } [TestFixture] @@ -61,22 +66,21 @@ public async Task TestBadgesSelf() var species1 = PkmnSpecies.RegisterName("1", "Einsmon"); var species2 = PkmnSpecies.RegisterName("22", "Zwozwomon"); var species3 = PkmnSpecies.RegisterName("13", "Drölfmon"); + + Badge badge1 = new Badge("1", user.Id, species1, Badge.BadgeSource.ManualCreation, Instant.FromUtc(1, 1, 1, 1, 1), null, false); + Badge badge2 = new Badge("2", user.Id, species2, Badge.BadgeSource.ManualCreation, Instant.FromUtc(1, 1, 1, 1, 1), "hisuian", false); + Badge badge3 = new Badge("3", user.Id, species3, Badge.BadgeSource.ManualCreation, Instant.FromUtc(1, 1, 1, 1, 1), null, true); + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(user.Id, null, null, null, false)).Returns( + Task.FromResult(new List() { badge1, badge2, badge2, badge3, badge3, badge3 })); + _userRepoMock .Setup(repo => repo.FindBySimpleName(user.SimpleName)) .ReturnsAsync(user); - _badgeRepoMock - .Setup(repo => repo.CountByUserPerSpecies(user.Id)) - .ReturnsAsync(new Dictionary - { - [species1] = 3, - [species2] = 6, - [species3] = 9, - }.ToImmutableSortedDictionary()); CommandResult result = await _badgeCommands.Badges(new CommandContext(MockMessage(user), ImmutableList.Empty, _argsParser)); - const string response = "Your badges: 3x #001 Einsmon, 9x #013 Drölfmon, 6x #022 Zwozwomon"; + const string response = "Your badges: 1x admin created #001 Einsmon, 3x admin created shiny #013 Drölfmon, 2x admin created hisuian #022 Zwozwomon"; Assert.That(result.Response, Is.EqualTo(response)); } @@ -84,28 +88,28 @@ public async Task TestBadgesSelf() public async Task TestBadgesOther() { User user = MockUser("MockUser"); + User otherUser = MockUser("Someone_Else"); var species1 = PkmnSpecies.RegisterName("1", "Einsmon"); var species2 = PkmnSpecies.RegisterName("22", "Zwozwomon"); var species3 = PkmnSpecies.RegisterName("13", "Drölfmon"); + + Badge badge1 = new Badge("1", otherUser.Id, species1, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, false); + Badge badge2 = new Badge("2", otherUser.Id, species2, Badge.BadgeSource.ManualCreation, Instant.MinValue, "hisuian", false); + Badge badge3 = new Badge("3", otherUser.Id, species3, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, true); + _argsParser.AddArgumentParser(new PkmnSpeciesParser(new[] { species1, species2, species3 })); - User otherUser = MockUser("Someone_Else"); _userRepoMock .Setup(repo => repo.FindBySimpleName(otherUser.SimpleName)) .ReturnsAsync(otherUser); - _badgeRepoMock - .Setup(repo => repo.CountByUserPerSpecies(otherUser.Id)) - .ReturnsAsync(new Dictionary - { - [species1] = 12, - [species2] = 23, - [species3] = 34, - }.ToImmutableSortedDictionary()); + + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(otherUser.Id, null, null, null, false)).Returns( + Task.FromResult(new List() { badge1, badge2, badge3 })); CommandResult result = await _badgeCommands.Badges(new CommandContext(MockMessage(user), ImmutableList.Create("sOmeOnE_eLsE"), _argsParser)); const string response = - "Someone_Else's badges: 12x #001 Einsmon, 34x #013 Drölfmon, 23x #022 Zwozwomon"; + "Someone_Else's badges: 1x admin created #001 Einsmon, 1x admin created shiny #013 Drölfmon, 1x admin created hisuian #022 Zwozwomon"; Assert.That(result.Response, Is.EqualTo(response)); } @@ -124,21 +128,21 @@ public async Task TestSpeciesOverNameIfAmbiguous() { User user = MockUser("MockUser"); PkmnSpecies species = PkmnSpecies.RegisterName("1", "PersonMon"); + Badge badge = new Badge("", user.Id, species, Badge.BadgeSource.RunCaught, Instant.MinValue, null, false); _argsParser.AddArgumentParser(new PkmnSpeciesParser(new[] { species })); User otherUser = MockUser("PersonMon"); _userRepoMock .Setup(repo => repo.FindBySimpleName(otherUser.SimpleName)) .ReturnsAsync(otherUser); - _badgeRepoMock - .Setup(repo => repo.CountByUserAndSpecies(user.Id, species)) - .ReturnsAsync(1); - _badgeRepoMock - .Setup(repo => repo.CountByUserPerSpecies(otherUser.Id)) - .ReturnsAsync(ImmutableSortedDictionary.Empty); + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(user.Id, species, null, null, false)).Returns( + Task.FromResult(new List() { badge })); + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(otherUser.Id, null, null, null, false)).Returns( + Task.FromResult(new List())); + CommandResult resultAmbiguous = await _badgeCommands.Badges(new CommandContext(MockMessage(user), ImmutableList.Create("PersonMon"), _argsParser)); - Assert.That(resultAmbiguous.Response, Is.EqualTo("You have 1x #001 PersonMon badges.")); + Assert.That(resultAmbiguous.Response, Is.EqualTo("Your badges: 1x run caught #001 PersonMon")); CommandResult resultDisambiguated = await _badgeCommands.Badges(new CommandContext(MockMessage(user), ImmutableList.Create("@PersonMon"), _argsParser)); @@ -150,6 +154,7 @@ public async Task TestSpeciesAndUserInAnyOrder() { User user = MockUser("User"); PkmnSpecies species = PkmnSpecies.RegisterName("1", "Species"); + Badge badge = new Badge("", user.Id, species, Badge.BadgeSource.Pinball, Instant.MinValue, null, false); _argsParser.AddArgumentParser(new PkmnSpeciesParser(new[] { species })); _userRepoMock .Setup(repo => repo.FindBySimpleName(user.SimpleName)) @@ -160,6 +165,10 @@ public async Task TestSpeciesAndUserInAnyOrder() _badgeRepoMock .Setup(repo => repo.CountByUserPerSpecies(user.Id)) .ReturnsAsync(ImmutableSortedDictionary.Empty); + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(user.Id, species, null, null, false)).Returns( + Task.FromResult(new List() { badge })); + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(user.Id, null, null, null, false)).Returns( + Task.FromResult(new List() { badge })); CommandResult result1 = await _badgeCommands.Badges(new CommandContext(MockMessage(user), ImmutableList.Create("Species", "User"), _argsParser)); @@ -167,7 +176,7 @@ public async Task TestSpeciesAndUserInAnyOrder() ImmutableList.Create("User", "Species"), _argsParser)); Assert.That(result2.Response, Is.EqualTo(result1.Response)); - Assert.That(result1.Response, Is.EqualTo("User has 1x #001 Species badges.")); + Assert.That(result1.Response, Is.EqualTo("User's badges: 1x pinball caught #001 Species")); } } @@ -264,11 +273,11 @@ public async Task TestGiftBadgeSuccessful() User user = MockUser("MockUser"); User recipient = MockUser("Recipient"); _userRepoMock.Setup(repo => repo.FindBySimpleName("recipient")).Returns(Task.FromResult((User?)recipient)); - Badge badge1 = new("badge1", user.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue); - Badge badge2 = new("badge2", user.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue); - Badge badge3 = new("badge3", user.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue); - _badgeRepoMock.Setup(repo => repo.FindByUserAndSpecies(user.Id, species, 2)) - .Returns(Task.FromResult(new List { badge1, badge2, badge3 })); + Badge badge1 = new("badge1", user.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, false); + Badge badge2 = new("badge2", user.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, false); + Badge badge3 = new("badge3", user.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, false); + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(user.Id, species, null, null, false)) + .Returns(Task.FromResult(new List { badge1, badge2, badge3, })); CommandResult result = await _badgeCommands.GiftBadge(new CommandContext(MockMessage(user), ImmutableList.Create("recipient", "species", "2"), _argsParser)); @@ -291,7 +300,7 @@ public async Task TestGiftBadgeNotOwned() User user = MockUser("MockUser"); User recipient = MockUser("Recipient"); _userRepoMock.Setup(repo => repo.FindBySimpleName("recipient")).Returns(Task.FromResult((User?)recipient)); - _badgeRepoMock.Setup(repo => repo.FindByUserAndSpecies(user.Id, species, 1)) + _badgeRepoMock.Setup(repo => repo.FindAllByCustom(user.Id, species, null, null, false)) .Returns(Task.FromResult(new List())); CommandResult result = await _badgeCommands.GiftBadge(new CommandContext(MockMessage(user), @@ -306,5 +315,46 @@ public async Task TestGiftBadgeNotOwned() It.IsAny>()), Times.Never); } + + [Test] + public async Task ListSellBadge_lists_callers_badges_when_user_isnt_specified() + { + User user1 = MockUser("u1"); + PkmnSpecies speciesA = PkmnSpecies.RegisterName("1", "a"); + Badge badgeA = new("badgeA", user1.Id, speciesA, Badge.BadgeSource.Pinball, Instant.MinValue, null, false) { SellPrice = 1 }; + + _badgeRepoMock.Setup(repo => repo.FindAllForSaleByCustom(user1.Id, speciesA, null, null, false)) + .Returns(Task.FromResult(new List() { badgeA })); + _userRepoMock.Setup(repo => repo.FindById(user1.Id)).Returns(Task.FromResult((User?)user1)); + + _argsParser.AddArgumentParser(new PkmnSpeciesParser(new[] { speciesA })); + + CommandResult result = await _badgeCommands.ListSellBadge(new CommandContext(MockMessage(user1), ImmutableList.Create("a"), _argsParser)); + + Assert.That(result.Response, Is.EqualTo("1 badges found: pinball caught #001 a sold by u1 for T1")); + } + + [Test] + public async Task ListSellBadge_lists_others_sold_badges() + { + User caller = MockUser("streamer"); + User seller = MockUser("notstreamer"); + PkmnSpecies species = PkmnSpecies.OfId("69"); + Badge.BadgeSource source = Badge.BadgeSource.Transmutation; + string? form = null; + bool shiny = false; + Badge badgeA = new Badge("A", seller.Id, species, source, Instant.MinValue, form, shiny) { SellPrice = 1 }; + Badge badgeB = new Badge("B", seller.Id, species, source, Instant.MinValue, form, shiny) { SellPrice = 1 }; + + _argsParser.AddArgumentParser(new PkmnSpeciesParser(new[] { species })); + _userRepoMock.Setup(repo => repo.FindBySimpleName("notstreamer")).Returns(Task.FromResult((User?)seller)); + _userRepoMock.Setup(repo => repo.FindById(seller.Id)).Returns(Task.FromResult((User?)seller)); + _badgeMarketRepoMock.Setup(repo => repo.FindAllBadgesForSale(seller.Id, null, null, null, shiny)) + .Returns(Task.FromResult(new List() { badgeA, badgeB })); + + CommandResult result = await _badgeCommands.ListSellBadge(new CommandContext(MockMessage(caller), ImmutableList.Create("notstreamer"), _argsParser)); + + Assert.That(result.Response, Is.EqualTo("2 badges found: 2x transmuted #069 ??? sold by notstreamer for T1")); + } } } diff --git a/tests/TPP.Core.Tests/Commands/Definitions/OperatorCommandsTest.cs b/tests/TPP.Core.Tests/Commands/Definitions/OperatorCommandsTest.cs index 0bbbb9b9..469551cd 100644 --- a/tests/TPP.Core.Tests/Commands/Definitions/OperatorCommandsTest.cs +++ b/tests/TPP.Core.Tests/Commands/Definitions/OperatorCommandsTest.cs @@ -165,11 +165,11 @@ public async Task TestTransferBadgeSuccessful() _userRepoMock.Object); _userRepoMock.Setup(repo => repo.FindBySimpleName("gifter")).Returns(Task.FromResult((User?)gifter)); _userRepoMock.Setup(repo => repo.FindBySimpleName("recipient")).Returns(Task.FromResult((User?)recipient)); - Badge badge1 = new("badge1", gifter.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue); - Badge badge2 = new("badge2", gifter.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue); - Badge badge3 = new("badge3", gifter.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue); + Badge badge1 = new("badge1", gifter.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, false); + Badge badge2 = new("badge2", gifter.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, false); + Badge badge3 = new("badge3", gifter.Id, species, Badge.BadgeSource.ManualCreation, Instant.MinValue, null, false); _badgeRepoMock.Setup(repo => repo.FindByUserAndSpecies(gifter.Id, species, 2)) - .Returns(Task.FromResult(new List { badge1, badge2, badge3 })); + .Returns(Task.FromResult(new List { badge1, badge2, badge3, })); CommandResult result = await operatorCommands.TransferBadge(new CommandContext(MockMessage(userSelf), ImmutableList.Create("gifter", "recipient", "species", "2", "because", "reason"), _argsParser)); @@ -264,10 +264,10 @@ public async Task TestCreateBadge() CommandResult result = await operatorCommands.CreateBadge(new CommandContext(MockMessage(user), ImmutableList.Create("recipient", "species", "123"), _argsParser)); - Assert.That(result.Response, Is.EqualTo("123 badges of species #001 Species created for user Recipient.")); + Assert.That(result.Response, Is.EqualTo("123 #001 Species badges created for Recipient.")); Assert.That(result.ResponseTarget, Is.EqualTo(ResponseTarget.Source)); _badgeRepoMock.Verify(repo => - repo.AddBadge(recipient.Id, species, Badge.BadgeSource.ManualCreation, null), + repo.AddBadge(recipient.Id, species, Badge.BadgeSource.ManualCreation, null, false, null), Times.Exactly(123)); } } diff --git a/tests/TPP.Persistence.MongoDB.Tests/Repos/BadgeMarketRepoTest.cs b/tests/TPP.Persistence.MongoDB.Tests/Repos/BadgeMarketRepoTest.cs new file mode 100644 index 00000000..a29a710f --- /dev/null +++ b/tests/TPP.Persistence.MongoDB.Tests/Repos/BadgeMarketRepoTest.cs @@ -0,0 +1,218 @@ +using Moq; +using NodaTime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TPP.Model; +using TPP.Persistence.MongoDB.Repos; +using MongoDB.Driver; +using NUnit.Framework; +using TPP.Common; +using TPP.Persistence; + +namespace TPP.Persistence.MongoDB.Tests.Repos +{ + class BadgeMarketRepoTest : MongoTestBase + { + + private Mock _mockUserRepo = null!; + private Mock> _mockBank = null!; + private Mock _mockBadgeRepo = null!; + + public IBadgeMarketRepo CreateBadgeMarketRepo() + { + IMongoDatabase db = CreateTemporaryDatabase(); + _mockUserRepo = new Mock(); + _mockBank = new Mock>(); + _mockBadgeRepo = new Mock(); + + BadgeMarketRepo badgeMarketRepo = new BadgeMarketRepo(db, _mockBadgeRepo.Object, _mockUserRepo.Object, _mockBank.Object, Mock.Of()); + return badgeMarketRepo; + } + + internal class MockClock : IClock + { + public Instant FixedCurrentInstant = Instant.FromUnixTimeSeconds(1234567890); + public Instant GetCurrentInstant() => FixedCurrentInstant; + } + + [Test] + public async Task CreateBuyOffer_write_then_read_are_equal() + { + string userId = "m4"; + PkmnSpecies species = PkmnSpecies.OfId("1"); + string form = ""; + Badge.BadgeSource source = Badge.BadgeSource.ManualCreation; + bool shiny = true; + int price = 999; + int amount = 1; + + IBadgeMarketRepo badgeMarketRepo = CreateBadgeMarketRepo(); + + _mockBadgeRepo.Setup(r => r.FindAllForSaleByCustom(null, species, null, null, shiny)).Returns(Task.FromResult(new List())); + + BadgeBuyOffer offer = await badgeMarketRepo.CreateBuyOffer(userId, species, form, source, shiny, price, amount, Instant.MaxValue); + + Assert.That(userId, Is.EqualTo(offer.UserId)); + Assert.That(species, Is.EqualTo(offer.Species)); + Assert.That(form, Is.EqualTo(offer.Form)); + Assert.That(source, Is.EqualTo(offer.Source)); + Assert.That(shiny, Is.EqualTo(offer.Shiny)); + Assert.That(price, Is.EqualTo(offer.Price)); + Assert.That(amount, Is.EqualTo(offer.Amount)); + } + + [Test] + public async Task CreateSellOffer_sells_newest_badge() + { + string userId = "thelegend27"; + PkmnSpecies species = PkmnSpecies.OfId("1"); + string form = ""; + Badge.BadgeSource source = Badge.BadgeSource.ManualCreation; + bool shiny = true; + int price = 999; + Instant before = Instant.FromUtc(2000, 1, 1, 0, 0); + Instant after = before.PlusNanoseconds(69); + + IBadgeMarketRepo badgeMarketRepo = CreateBadgeMarketRepo(); + + Badge badgeA = new Badge("A", userId, species, source, before, form, shiny); + Badge badgeB = new Badge("B", userId, species, source, after, form, shiny); + Badge badgeBForSale = new Badge("B", userId, species, source, after, form, shiny) { SellPrice = price }; + List notForSale = new List { badgeA, badgeB }; + + _mockBadgeRepo.Setup(m => m.FindAllNotForSaleByCustom(userId, species, form, source, shiny)).Returns(Task.FromResult(notForSale)); + _mockBadgeRepo.Setup(m => m.FindAllForSaleByCustom(null, species, null, null, shiny)).Returns(Task.FromResult(new List())); + _mockBadgeRepo.Setup(m => m.SetBadgeSellPrice(badgeB, price)).Returns(Task.FromResult(badgeBForSale)); + + Badge selling = await badgeMarketRepo.CreateSellOffer(userId, species, form, source, shiny, price); + + Assert.That(after, Is.EqualTo(selling.CreatedAt)); + } + + [Test] + public async Task CreateSellOffer_prioritizes_duplicates_of_forms_when_form_unspecified() + { + string userId = "shellosLuvr"; + PkmnSpecies species = PkmnSpecies.OfId("422"); + string formWest = "West Sea"; + string formEast = "East Sea"; + Badge.BadgeSource source = Badge.BadgeSource.RunCaught; + bool shiny = false; + Instant time = Instant.FromUtc(2006, 8, 28, 1, 0); + int price = 999999; + + Badge shellosWest = new Badge("A", userId, species, source, time.PlusNanoseconds(9000), formWest, shiny); + Badge shellosEastOlder = new Badge("B", userId, species, source, time.PlusNanoseconds(0), formEast, shiny); + Badge shellosEastNewer = new Badge("C", userId, species, source, time.PlusNanoseconds(1), formEast, shiny); + Badge shellosEastSelling = new Badge("C_sell", userId, species, source, time.PlusNanoseconds(1), formEast, shiny) { SellPrice = price }; + + IBadgeMarketRepo badgeMarketRepo = CreateBadgeMarketRepo(); + + List notForSale = new List() { shellosWest, shellosEastOlder, shellosEastNewer }; + + _mockBadgeRepo.Setup(m => m.FindAllNotForSaleByCustom(userId, species, null, source, shiny)).Returns(Task.FromResult(notForSale)); + _mockBadgeRepo.Setup(m => m.FindAllForSaleByCustom(null, species, null, null, shiny)).Returns(Task.FromResult(new List())); + _mockBadgeRepo.Setup(m => m.SetBadgeSellPrice(shellosEastNewer, price)).Returns(Task.FromResult(shellosEastSelling)); + + Badge selling = await badgeMarketRepo.CreateSellOffer(userId, species, null, source, shiny, price); + + Assert.That(selling.Id, Is.EqualTo(shellosEastSelling.Id)); + } + + [Test] + public async Task ResolveBuyOffers_fills_outstanding_buy_offer() + { + string sellerId = "seller"; + string buyerId = "buyer"; + PkmnSpecies species = PkmnSpecies.OfId("1"); + Badge.BadgeSource source = Badge.BadgeSource.RunCaught; + string? form = null; + bool shiny = false; + Instant time = Instant.FromUtc(1996, 2, 27, 0, 0); + int price = 1; + int amount = 1; + User buyer = new User(buyerId, buyerId, buyerId, buyerId, null, Instant.MinValue, Instant.MinValue, null, 1000, price); + User seller = new User(sellerId, sellerId, sellerId, sellerId, null, Instant.MinValue, Instant.MinValue, null, 1000, 0); + Badge badgeA = new Badge("A", sellerId, species, source, time, form, shiny); + Badge badgeASelling = new Badge("A", sellerId, species, source, time, form, shiny) { SellPrice = price, SellingSince = time }; + + IBadgeMarketRepo badgeMarketRepo = CreateBadgeMarketRepo(); + + List sellerBadgesNotForSale = new List() { badgeA }; + _mockBadgeRepo.Setup(m => m.FindAllNotForSaleByCustom(sellerId, species, form, source, shiny)).Returns(Task.FromResult(sellerBadgesNotForSale)); + _mockBadgeRepo.Setup(m => m.FindAllForSaleByCustom(null, species, null, null, shiny)).Returns(Task.FromResult(new List())); + _mockBadgeRepo.Setup(m => m.SetBadgeSellPrice(badgeA, price)).Returns(Task.FromResult(badgeASelling)); + Badge selling = await badgeMarketRepo.CreateSellOffer(sellerId, species, form, source, shiny, price); + + _mockBadgeRepo.Setup(m => m.FindAllForSaleByCustom(null, species, null, null, shiny)).Returns(Task.FromResult(new List() { selling })); + _mockUserRepo.Setup(m => m.FindById(buyerId)).Returns(Task.FromResult((User?)buyer)); + _mockUserRepo.Setup(m => m.FindById(sellerId)).Returns(Task.FromResult((User?)seller)); + _mockBadgeRepo.Setup(m => m.FindAllForSaleByCustom(sellerId, species, form, null, shiny)).Returns(Task.FromResult(new List())); + + await badgeMarketRepo.CreateBuyOffer(buyerId, species, form, source, shiny, price, amount, time.PlusNanoseconds(2)); + + List remainingBuyOffers = await badgeMarketRepo.FindAllBuyOffers(buyerId, species, form, source, shiny); + Assert.That(remainingBuyOffers.Count, Is.EqualTo(1)); + + var soldBadges = await badgeMarketRepo.ResolveBuyOffers(species, shiny); + + remainingBuyOffers = await badgeMarketRepo.FindAllBuyOffers(buyerId, species, form, source, shiny); + Assert.That(remainingBuyOffers.Count, Is.EqualTo(0)); + Assert.That(soldBadges.Count, Is.EqualTo(1)); + Assert.That(soldBadges[0].seller, Is.EqualTo(seller)); + Assert.That(soldBadges[0].buyer, Is.EqualTo(buyer)); + Assert.That(soldBadges[0].soldBadge, Is.EqualTo(badgeASelling)); + Assert.That(soldBadges[0].price, Is.EqualTo(price)); + } + + [Test] + public async Task DeleteBuyOffer_removes_outstanding_buy_offer() + { + string buyerId = "buyer"; + PkmnSpecies species = PkmnSpecies.OfId("1"); + Badge.BadgeSource? source = null; + string? form = null; + bool shiny = false; + Instant time = Instant.FromUtc(0, 1, 1, 0, 0); + int price = 1; + int amount = 1; + + IBadgeMarketRepo badgeMarketRepo = CreateBadgeMarketRepo(); + _mockBadgeRepo.Setup(r => r.FindAllForSaleByCustom(null, species, null, null, shiny)).Returns(Task.FromResult(new List())); + + await badgeMarketRepo.CreateBuyOffer(buyerId, species, form, source, shiny, price, amount, time); + Assert.That(badgeMarketRepo.FindAllBuyOffers(buyerId, species, form, source, shiny).Result.Count, Is.EqualTo(1)); + + await badgeMarketRepo.DeleteBuyOffer(buyerId, species, form, source, shiny, amount); + Assert.That(badgeMarketRepo.FindAllBuyOffers(buyerId, species, form, source, shiny).Result.Count, Is.EqualTo(0)); + } + + [Test] + public async Task DeleteSellOffer_removes_outstanding_sell_offer() + { + string sellerId = "buyer"; + PkmnSpecies species = PkmnSpecies.OfId("1"); + Badge.BadgeSource source = Badge.BadgeSource.Pinball; + string? form = null; + bool shiny = false; + Instant time = Instant.FromUtc(0, 1, 1, 0, 0); + long price = 1; + Badge badgeForSale = new Badge("A", sellerId, species, source, time, form, shiny) { SellingSince = time, SellPrice = price }; + Badge badgeNotForSale = new Badge("A", sellerId, species, source, time, form, shiny); + + IBadgeMarketRepo badgeMarketRepo = CreateBadgeMarketRepo(); + + _mockBadgeRepo.Setup(r => r.FindAllForSaleByCustom(sellerId, species, form, source, shiny)).Returns(Task.FromResult(new List() { badgeForSale })); + _mockBadgeRepo.Setup(r => r.SetBadgeSellPrice(badgeForSale, 0)).Returns(Task.FromResult(badgeNotForSale)); + await badgeMarketRepo.DeleteSellOffer(sellerId, species, form, source, shiny, 1); + + _mockBadgeRepo.Setup(r => r.FindAllForSaleByCustom(sellerId, species, form, source, shiny)).Returns(Task.FromResult(new List())); + List badgesForSale = await badgeMarketRepo.FindAllBadgesForSale(sellerId, species, form, source, shiny); + + Assert.That(badgesForSale.Count, Is.EqualTo(0)); + } + } +} diff --git a/tests/TPP.Persistence.MongoDB.Tests/Repos/BadgeRepoTest.cs b/tests/TPP.Persistence.MongoDB.Tests/Repos/BadgeRepoTest.cs index 584ac8d3..a5934b85 100644 --- a/tests/TPP.Persistence.MongoDB.Tests/Repos/BadgeRepoTest.cs +++ b/tests/TPP.Persistence.MongoDB.Tests/Repos/BadgeRepoTest.cs @@ -5,6 +5,7 @@ using MongoDB.Driver; using Moq; using NodaTime; +using NodaTime.Text; using NUnit.Framework; using TPP.Common; using TPP.Model; @@ -18,12 +19,18 @@ public class BadgeRepoTest : MongoTestBase public BadgeRepo CreateBadgeRepo() => new BadgeRepo(CreateTemporaryDatabase(), Mock.Of(), Mock.Of()); + internal class MockClock : IClock + { + public Instant FixedCurrentInstant = Instant.FromUnixTimeSeconds(1234567890); + public Instant GetCurrentInstant() => FixedCurrentInstant; + } + [Test] public async Task insert_then_read_are_equal() { BadgeRepo badgeRepo = CreateBadgeRepo(); // when - Badge badge = await badgeRepo.AddBadge(null, PkmnSpecies.OfId("16"), Badge.BadgeSource.ManualCreation); + Badge badge = await badgeRepo.AddBadge(null, PkmnSpecies.OfId("16"), Badge.BadgeSource.ManualCreation, null, false); // then Assert.That(badge.Id, Is.Not.EqualTo(string.Empty)); @@ -42,8 +49,8 @@ public async Task insert_sets_current_timestamp_as_creation_date() IBadgeRepo badgeRepo = new BadgeRepo( CreateTemporaryDatabase(), Mock.Of(), clockMock.Object); - Badge badge = await badgeRepo.AddBadge(null, PkmnSpecies.OfId("16"), Badge.BadgeSource.ManualCreation); - Assert.That(badge.CreatedAt, Is.EqualTo(createdAt)); + Badge badge = await badgeRepo.AddBadge(null, PkmnSpecies.OfId("16"), Badge.BadgeSource.ManualCreation, null, false); + Assert.That(createdAt, Is.EqualTo(badge.CreatedAt)); } /// @@ -56,7 +63,7 @@ public async Task has_expected_bson_datatypes() BadgeRepo badgeRepo = CreateBadgeRepo(); // when PkmnSpecies randomSpecies = PkmnSpecies.OfId("9001"); - Badge badge = await badgeRepo.AddBadge(null, randomSpecies, Badge.BadgeSource.RunCaught); + Badge badge = await badgeRepo.AddBadge(null, randomSpecies, Badge.BadgeSource.RunCaught, null, false); // then IMongoCollection badgesCollectionBson = @@ -73,15 +80,15 @@ public async Task can_find_by_user() { IBadgeRepo badgeRepo = CreateBadgeRepo(); // given - Badge badgeUserA1 = await badgeRepo.AddBadge("userA", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball); - Badge badgeUserA2 = await badgeRepo.AddBadge("userA", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball); - Badge badgeUserB = await badgeRepo.AddBadge("userB", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - Badge badgeNobody = await badgeRepo.AddBadge(null, PkmnSpecies.OfId("4"), Badge.BadgeSource.Pinball); + Badge badgeUserA1 = await badgeRepo.AddBadge("userA", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball, null, false); + Badge badgeUserA2 = await badgeRepo.AddBadge("userA", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball, null, false); + Badge badgeUserB = await badgeRepo.AddBadge("userB", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + Badge badgeNobody = await badgeRepo.AddBadge(null, PkmnSpecies.OfId("4"), Badge.BadgeSource.Pinball, null, false); // when - List resultUserA = await badgeRepo.FindByUser("userA"); - List resultUserB = await badgeRepo.FindByUser("userB"); - List resultNobody = await badgeRepo.FindByUser(null); + List resultUserA = await badgeRepo.FindAllByUser("userA"); + List resultUserB = await badgeRepo.FindAllByUser("userB"); + List resultNobody = await badgeRepo.FindAllByUser(null); // then Assert.That(resultUserA, Is.EqualTo(new List { badgeUserA1, badgeUserA2 })); @@ -94,13 +101,13 @@ public async Task can_count_by_user_and_species() { IBadgeRepo badgeRepo = CreateBadgeRepo(); // given - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); // when long countHasNone = await badgeRepo.CountByUserAndSpecies("user", PkmnSpecies.OfId("1")); @@ -118,13 +125,13 @@ public async Task can_count_per_species_for_one_user() { IBadgeRepo badgeRepo = CreateBadgeRepo(); // given - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); // when ImmutableSortedDictionary result = await badgeRepo.CountByUserPerSpecies("user"); @@ -143,12 +150,12 @@ public async Task can_check_if_user_has_badge() { IBadgeRepo badgeRepo = CreateBadgeRepo(); // given - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", PkmnSpecies.OfId("3"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("userOther", PkmnSpecies.OfId("2"), Badge.BadgeSource.Pinball, null, false); // when bool hasUserSpecies1 = await badgeRepo.HasUserBadge("user", PkmnSpecies.OfId("1")); @@ -163,6 +170,61 @@ public async Task can_check_if_user_has_badge() Assert.IsFalse(hasUserSpecies4); } + [Test] + public async Task can_set_badge_sell_price() + { + IBadgeRepo badgeRepo = CreateBadgeRepo(); + Badge badge = await badgeRepo.AddBadge("user", PkmnSpecies.OfId("1"), Badge.BadgeSource.Pinball, null, false); + + Badge forSale = await badgeRepo.SetBadgeSellPrice(badge, 10); + + Assert.That(forSale.SellPrice, Is.EqualTo(10)); + } + + [Test] + public async Task FindAllForSaleByCustom_only_returns_badges_for_sale() + { + IBadgeRepo badgeRepo = CreateBadgeRepo(); + PkmnSpecies species = PkmnSpecies.OfId("1"); + Badge notForSale = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false); + + Badge forSale = await badgeRepo.SetBadgeSellPrice(notForSale, 1); + + List saleList = await badgeRepo.FindAllForSaleByCustom(null, species, null, null, null); + + Assert.That(saleList, Is.EqualTo(new List() { forSale })); + } + + [Test] + public async Task can_handle_null_shiny_fields() + { + IMongoDatabase db = CreateTemporaryDatabase(); + + const string id = "590df61373b975210006fcdf"; + Instant instant = InstantPattern.ExtendedIso.Parse("2017-05-06T16:13:07.314Z").Value; + + BadgeRepo badgeRepo = new BadgeRepo(db, new BadgeLogRepo(db), new MockClock()); + IMongoCollection bsonBadgeCollection = db.GetCollection("badges"); + await bsonBadgeCollection.InsertOneAsync(BsonDocument.Create(new Dictionary + { + ["_id"] = ObjectId.Parse(id), + ["user"] = "mogi", + ["species"] = "1", + ["source"] = "manual_creation", + ["created_at"] = instant.ToDateTimeUtc(), + ["form"] = null, + })); + + IMongoCollection badgeCollection = db.GetCollection("badges"); ; + + Badge b = await badgeCollection.Find(b => b.Id == id).FirstAsync(); + Assert.That(b.Shiny, Is.EqualTo(false)); + + List badges = await badgeRepo.FindAllByCustom(null, null, null, null, false); + Assert.That(badges.Count, Is.EqualTo(1)); + } + [TestFixture] private class TransferBadge : MongoTestBase { @@ -172,7 +234,7 @@ public async Task returns_updated_badge_object() IBadgeRepo badgeRepo = new BadgeRepo( CreateTemporaryDatabase(), Mock.Of(), Mock.Of()); Badge badge = await badgeRepo.AddBadge( - "user", PkmnSpecies.OfId("1"), Badge.BadgeSource.ManualCreation); + "user", PkmnSpecies.OfId("1"), Badge.BadgeSource.ManualCreation, null, false); IImmutableList updatedBadges = await badgeRepo.TransferBadges( ImmutableList.Create(badge), "recipient", "reason", new Dictionary()); @@ -183,6 +245,7 @@ public async Task returns_updated_badge_object() Assert.That(updatedBadges[0].Source, Is.EqualTo(badge.Source)); Assert.That(updatedBadges[0].CreatedAt, Is.EqualTo(badge.CreatedAt)); Assert.That(updatedBadges[0].UserId, Is.EqualTo("recipient")); + Assert.That(updatedBadges[0].Form, Is.EqualTo(badge.Form)); } [Test] @@ -190,7 +253,7 @@ public async Task unmarks_as_selling() { BadgeRepo badgeRepo = new(CreateTemporaryDatabase(), Mock.Of(), Mock.Of()); Badge badge = await badgeRepo.AddBadge( - "user", PkmnSpecies.OfId("1"), Badge.BadgeSource.ManualCreation); + "user", PkmnSpecies.OfId("1"), Badge.BadgeSource.ManualCreation, null, false); await badgeRepo.Collection.UpdateOneAsync( Builders.Filter.Where(b => b.Id == badge.Id), Builders.Update @@ -216,7 +279,7 @@ public async Task logs_to_badgelog() Mock mongoBadgeLogRepoMock = new(); BadgeRepo badgeRepo = new(CreateTemporaryDatabase(), mongoBadgeLogRepoMock.Object, clockMock.Object); Badge badge = await badgeRepo.AddBadge( - "user", PkmnSpecies.OfId("1"), Badge.BadgeSource.ManualCreation); + "user", PkmnSpecies.OfId("1"), Badge.BadgeSource.ManualCreation, null, false); Instant timestamp = Instant.FromUnixTimeSeconds(123); clockMock.Setup(c => c.GetCurrentInstant()).Returns(timestamp); @@ -235,8 +298,8 @@ public async Task triggers_species_lost_event() Mock mongoBadgeLogRepoMock = new(); BadgeRepo badgeRepo = new(CreateTemporaryDatabase(), mongoBadgeLogRepoMock.Object, Mock.Of()); PkmnSpecies species = PkmnSpecies.OfId("1"); - Badge badge1 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation); - Badge badge2 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation); + Badge badge1 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation, null, false); + Badge badge2 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation, null, false); int userLostBadgeInvocations = 0; badgeRepo.UserLostBadgeSpecies += (_, args) => { @@ -259,8 +322,8 @@ public async Task aborts_all_transfers_if_one_fails() Mock mongoBadgeLogRepoMock = new(); BadgeRepo badgeRepo = new(CreateTemporaryDatabase(), mongoBadgeLogRepoMock.Object, Mock.Of()); PkmnSpecies species = PkmnSpecies.OfId("1"); - Badge badge1 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation); - Badge badge2 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation); + Badge badge1 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation, null, false); + Badge badge2 = await badgeRepo.AddBadge("user", species, Badge.BadgeSource.ManualCreation, null, false); // make in-memory badge reference stale to cause the transfer to fail on the second badge await badgeRepo.Collection.UpdateOneAsync( Builders.Filter.Where(b => b.Id == badge2.Id), @@ -301,10 +364,10 @@ public async Task ignore_lapsed_for_rarity_counts() clockMock.Object, lastRarityUpdate: tUpdate, rarityCalculationTransition: transition); PkmnSpecies species = PkmnSpecies.OfId("1-testlapsed"); - await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, tBeforeUpdateLapsed); - await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, tBeforeUpdateConsidered); - await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, tUpdate); - await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, tNow); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false, tBeforeUpdateLapsed); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false, tBeforeUpdateConsidered); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false, tUpdate); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false, tNow); await badgeRepo.RenewBadgeStats(onlyTheseSpecies: ImmutableHashSet.Create(species)); ImmutableSortedDictionary stats = await badgeRepo.GetBadgeStats(); @@ -321,9 +384,9 @@ public async Task ignore_destroyed_for_regular_count() Mock.Of()); PkmnSpecies species = PkmnSpecies.OfId("1-testdestroyed"); - await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false); // second badge has no owner - await badgeRepo.AddBadge(null, species, Badge.BadgeSource.Pinball); + await badgeRepo.AddBadge(null, species, Badge.BadgeSource.Pinball, null, false); await badgeRepo.RenewBadgeStats(onlyTheseSpecies: ImmutableHashSet.Create(species)); ImmutableSortedDictionary stats = await badgeRepo.GetBadgeStats(); @@ -340,9 +403,9 @@ public async Task ignore_unnatural_sources_for_generated_count() Mock.Of()); PkmnSpecies species = PkmnSpecies.OfId("1-testunnatural"); - await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Pinball, null, false); // second badge has source 'transmutation', which is not a natural source - await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Transmutation); + await badgeRepo.AddBadge("user", species, Badge.BadgeSource.Transmutation, null, false); await badgeRepo.RenewBadgeStats(onlyTheseSpecies: ImmutableHashSet.Create(species)); ImmutableSortedDictionary stats = await badgeRepo.GetBadgeStats(); @@ -362,17 +425,17 @@ public async Task incorporate_source_and_existence_in_rarity() PkmnSpecies species2 = PkmnSpecies.OfId("2-testrarity"); PkmnSpecies species3 = PkmnSpecies.OfId("3-testrarity"); - await badgeRepo.AddBadge("user", species1, Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", species1, Badge.BadgeSource.Transmutation); + await badgeRepo.AddBadge("user", species1, Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", species1, Badge.BadgeSource.Transmutation, null, false); - await badgeRepo.AddBadge("user", species2, Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", species2, Badge.BadgeSource.Transmutation); - await badgeRepo.AddBadge("user", species2, Badge.BadgeSource.Transmutation); - await badgeRepo.AddBadge(null, species2, Badge.BadgeSource.Transmutation); + await badgeRepo.AddBadge("user", species2, Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", species2, Badge.BadgeSource.Transmutation, null, false); + await badgeRepo.AddBadge("user", species2, Badge.BadgeSource.Transmutation, null, false); + await badgeRepo.AddBadge(null, species2, Badge.BadgeSource.Transmutation, null, false); - await badgeRepo.AddBadge(null, species3, Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", species3, Badge.BadgeSource.Pinball); - await badgeRepo.AddBadge("user", species3, Badge.BadgeSource.Transmutation); + await badgeRepo.AddBadge(null, species3, Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", species3, Badge.BadgeSource.Pinball, null, false); + await badgeRepo.AddBadge("user", species3, Badge.BadgeSource.Transmutation, null, false); await badgeRepo.RenewBadgeStats(); ImmutableSortedDictionary stats = await badgeRepo.GetBadgeStats();