diff --git a/test/PatternKit.Examples.Tests/Chain/MediatedTransactionPipelineDemoTests.cs b/test/PatternKit.Examples.Tests/Chain/MediatedTransactionPipelineDemoTests.cs index e091edf9..6a0a4b4e 100644 --- a/test/PatternKit.Examples.Tests/Chain/MediatedTransactionPipelineDemoTests.cs +++ b/test/PatternKit.Examples.Tests/Chain/MediatedTransactionPipelineDemoTests.cs @@ -1,4 +1,5 @@ using PatternKit.Examples.Chain; +using PatternKit.Examples.Chain.ConfigDriven; using TinyBDD; using TinyBDD.Xunit; using Xunit.Abstractions; @@ -281,6 +282,171 @@ public void ComputeDelta_Rounds_To_Nearest_Nickel() } } +public sealed class MediatedTransactionPipelineCoverageTests +{ + [Scenario("TransactionPipelineBuilder FluentHooks Cover CustomStagesRulesAndCouponDiscounts")] + [Fact] + public void TransactionPipelineBuilder_FluentHooks_Cover_CustomStagesRulesAndCouponDiscounts() + { + var couponContext = new TransactionContext + { + Customer = new Customer(null, 30), + Items = + [ + new LineItem("MFG", 10m, Qty: 2, ManufacturerCoupon: 1m), + new LineItem("HOUSE", 5m, Qty: 1, InHouseCoupon: 0.50m), + ] + }; + var couponPipeline = TransactionPipelineBuilder.New() + .WithRoundingRules() + .AddDiscountsAndTax() + .Build(); + + var couponResult = couponPipeline.Run(couponContext); + + ScenarioExpect.True(couponResult.Result.Ok); + ScenarioExpect.Contains(couponResult.Ctx.Log, entry => entry.StartsWith("discount: manufacturer coupons", StringComparison.Ordinal)); + ScenarioExpect.Contains(couponResult.Ctx.Log, entry => entry.StartsWith("discount: in-house coupons", StringComparison.Ordinal)); + + var customContext = new TransactionContext + { + Customer = new Customer(null, 30), + Items = [new LineItem("STOP", 1m)] + }; + var customPipeline = TransactionPipelineBuilder.New() + .AddStage(ctx => + { + ctx.Result = TxResult.Fail("custom", "custom stop"); + return false; + }) + .Build(); + + var customResult = customPipeline.Run(customContext); + + ScenarioExpect.False(customResult.Result.Ok); + ScenarioExpect.Equal("custom", customResult.Result.Code); + } + + [Scenario("TransactionPipelineBuilder Preauth Blocks EmptyBaskets")] + [Fact] + public void TransactionPipelineBuilder_Preauth_Blocks_EmptyBaskets() + { + var context = new TransactionContext + { + Customer = new Customer(null, 30), + Items = [] + }; + var pipeline = TransactionPipelineBuilder.New() + .AddPreauth() + .Build(); + + var result = pipeline.Run(context); + + ScenarioExpect.False(result.Result.Ok); + ScenarioExpect.Equal("empty", result.Result.Code); + ScenarioExpect.True(result.Ctx.Log.Contains("preauth: empty basket")); + } + + [Scenario("CardTenderHandlers Surface AuthorizationAndCaptureFailures")] + [Fact] + public void CardTenderHandlers_Surface_AuthorizationAndCaptureFailures() + { + var authProcessor = new ConfigurableProcessor( + TxResult.Fail("declined", "declined"), + TxResult.Success("capture")); + var captureProcessor = new ConfigurableProcessor( + TxResult.Success("auth"), + TxResult.Fail("capture-failed", "capture failed")); + var authContext = CreateContext(20m); + var captureContext = CreateContext(20m); + + var configAuthTender = new CardTender(new CardProcessors(new() + { + [CardVendor.Unknown] = authProcessor + })); + var configCaptureTender = new CardTender(new CardProcessors(new() + { + [CardVendor.Unknown] = captureProcessor + })); + var strategyAuthTender = new CardTenderStrategy(new CardProcessors(new() + { + [CardVendor.Unknown] = authProcessor + })); + var strategyCaptureTender = new CardTenderStrategy(new CardProcessors(new() + { + [CardVendor.Unknown] = captureProcessor + })); + + var tender = new Tender(PaymentKind.Card, Vendor: CardVendor.Unknown); + var configAuthResult = configAuthTender.Handle(authContext, tender); + var configCaptureResult = configCaptureTender.Handle(captureContext, tender); + var strategyAuthResult = strategyAuthTender.TryApply(CreateContext(20m), tender); + var strategyCaptureResult = strategyCaptureTender.TryApply(CreateContext(20m), tender); + + ScenarioExpect.Equal("tender:card", configAuthTender.Key); + ScenarioExpect.False(configAuthResult.Ok); + ScenarioExpect.Equal("declined", configAuthResult.Code); + ScenarioExpect.True(authContext.Log.Contains("auth: declined (declined)")); + ScenarioExpect.False(configCaptureResult.Ok); + ScenarioExpect.Equal("capture-failed", configCaptureResult.Code); + ScenarioExpect.True(captureContext.Log.Contains("auth: capture failed")); + ScenarioExpect.NotNull(strategyAuthResult); + ScenarioExpect.Equal("declined", strategyAuthResult!.Value.Code); + ScenarioExpect.NotNull(strategyCaptureResult); + ScenarioExpect.Equal("capture-failed", strategyCaptureResult!.Value.Code); + } + + [Scenario("CharityRoundUpRule NotifiesTrackerWhenApplied")] + [Fact] + public void CharityRoundUpRule_NotifiesTracker_WhenApplied() + { + var tracker = new RecordingCharityTracker(); + var rule = new CharityRoundUpRule(tracker); + var context = new TransactionContext + { + Customer = new Customer(null, 30), + Items = [new LineItem("CHARITY:KidsFund", 10.25m)] + }; + context.RecomputeSubtotal(); + + RoundingPipeline.Apply(context, [rule]); + + ScenarioExpect.Equal("KidsFund", tracker.Charity); + ScenarioExpect.Equal(0.75m, tracker.Delta); + ScenarioExpect.Contains(context.Log, entry => entry.StartsWith("charity: KidsFund notified", StringComparison.Ordinal)); + } + + private static TransactionContext CreateContext(decimal price) + { + var context = new TransactionContext + { + Customer = new Customer(null, 30), + Items = [new LineItem("CARD", price)] + }; + context.RecomputeSubtotal(); + return context; + } + + private sealed class ConfigurableProcessor(TxResult authorization, TxResult capture) : ICardProcessor + { + public TxResult Authorize(TransactionContext ctx) => authorization; + + public TxResult Capture(TransactionContext ctx) => capture; + } + + private sealed class RecordingCharityTracker : ICharityTracker + { + public string? Charity { get; private set; } + public decimal Delta { get; private set; } + + public void Track(string charity, Guid transactionId, decimal delta, decimal newTotal) + { + Charity = charity; + Delta = delta; + } + } +} + [Collection("Culture")] [Feature("Mediated Transaction pipeline – cash + loyalty + cigarettes")] diff --git a/test/PatternKit.Examples.Tests/ContextMapDemo/CommerceContextMapDemoTests.cs b/test/PatternKit.Examples.Tests/ContextMapDemo/CommerceContextMapDemoTests.cs index 71d619b3..bb5f1fa0 100644 --- a/test/PatternKit.Examples.Tests/ContextMapDemo/CommerceContextMapDemoTests.cs +++ b/test/PatternKit.Examples.Tests/ContextMapDemo/CommerceContextMapDemoTests.cs @@ -36,12 +36,26 @@ public Task Commerce_Context_Map_Imports_Through_IServiceCollection() services.AddCommerceContextMapDemo(); return services.BuildServiceProvider(); }) - .When("summarizing the context map", sp => sp.GetRequiredService().Summarize()) - .Then("the summary reflects the production relationships", summary => + .When("summarizing the context map and translating catalog products", sp => { - ScenarioExpect.Equal(2, summary.RelationshipCount); - ScenarioExpect.True(summary.HasPublishedLanguage); - ScenarioExpect.True(summary.HasCustomerSupplier); + var summary = sp.GetRequiredService().Summarize(); + var translator = sp.GetRequiredService(); + var product = translator.Translate(new CommerceContextMapDemo.CatalogProduct("SKU-42", "Trail Jacket")); + var shipment = new CommerceContextMapDemo.BillingShipment("SHIP-42", 9.95m); + return new { Summary = summary, Product = product, Shipment = shipment }; + }) + .Then("the summary reflects the production relationships", result => + { + ScenarioExpect.Equal(2, result.Summary.RelationshipCount); + ScenarioExpect.True(result.Summary.HasPublishedLanguage); + ScenarioExpect.True(result.Summary.HasCustomerSupplier); + }) + .And("the translator maps catalog language into fulfillment language", result => + { + ScenarioExpect.Equal("SKU-42", result.Product.Sku); + ScenarioExpect.Equal("Trail Jacket", result.Product.Description); + ScenarioExpect.Equal("SHIP-42", result.Shipment.ShipmentId); + ScenarioExpect.Equal(9.95m, result.Shipment.Charge); }) .AssertPassed(); diff --git a/test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs b/test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs index 2d115a03..50093ea5 100644 --- a/test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs +++ b/test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs @@ -134,6 +134,14 @@ public Task IoC_Registered_Examples_Can_Be_Used_By_Importing_Applications() var productCacheAside = provider.GetRequiredService(); var productRateLimit = provider.GetRequiredService(); var externalConfiguration = provider.GetRequiredService(); + var identity = provider.GetRequiredService(); + var presence = provider.GetRequiredService(); + var rateLimiter = provider.GetRequiredService(); + var preferences = provider.GetRequiredService(); + var email = provider.GetRequiredService(); + var sms = provider.GetRequiredService(); + var push = provider.GetRequiredService(); + var im = provider.GetRequiredService(); auth.Chain.Execute(new PatternKit.Examples.Chain.HttpRequest("GET", "/admin/metrics", new Dictionary())); @@ -157,6 +165,24 @@ public Task IoC_Registered_Examples_Can_Be_Used_By_Importing_Applications() var send = notifications.Strategy.ExecuteAsync(new SendContext(Guid.NewGuid(), "hello", false), CancellationToken.None) .GetAwaiter() .GetResult(); + var userId = Guid.NewGuid(); + var directEmail = email.SendAsync(new SendContext(userId, "email", false), CancellationToken.None).GetAwaiter().GetResult(); + var directSms = sms.SendAsync(new SendContext(userId, "sms", true), CancellationToken.None).GetAwaiter().GetResult(); + var directPush = push.SendAsync(new SendContext(userId, "push", false), CancellationToken.None).GetAwaiter().GetResult(); + var directIm = im.SendAsync(new SendContext(userId, "im", false), CancellationToken.None).GetAwaiter().GetResult(); + var preferredChannels = preferences.GetPreferredOrderAsync(userId, CancellationToken.None).GetAwaiter().GetResult(); + var identityReady = + identity.HasVerifiedEmailAsync(userId, CancellationToken.None).GetAwaiter().GetResult() + && identity.HasSmsOptInAsync(userId, CancellationToken.None).GetAwaiter().GetResult() + && identity.HasPushTokenAsync(userId, CancellationToken.None).GetAwaiter().GetResult(); + var presenceReady = + presence.IsOnlineInImAsync(userId, CancellationToken.None).GetAwaiter().GetResult() + && !presence.IsDoNotDisturbAsync(userId, CancellationToken.None).GetAwaiter().GetResult(); + var allChannelsRateLimited = + rateLimiter.CanSendAsync(Channel.Email, userId, CancellationToken.None).GetAwaiter().GetResult() + && rateLimiter.CanSendAsync(Channel.Sms, userId, CancellationToken.None).GetAwaiter().GetResult() + && rateLimiter.CanSendAsync(Channel.Push, userId, CancellationToken.None).GetAwaiter().GetResult() + && rateLimiter.CanSendAsync(Channel.Im, userId, CancellationToken.None).GetAwaiter().GetResult(); var state = asyncState.RunAsync(["connect", "ok"]).GetAwaiter().GetResult(); var asyncResult = asyncTemplate.Pipeline.ExecuteAsync(7, CancellationToken.None).GetAwaiter().GetResult(); var generatedRecipientList = generatedRecipients.Runner.RunGenerated(); @@ -223,7 +249,15 @@ public Task IoC_Registered_Examples_Can_Be_Used_By_Importing_Applications() ("generated queue load leveling accepts fulfillment work", queueLoadLeveling.Service.EnqueueAsync(new FulfillmentWorkItem("ORDER-QL", "central")).GetAwaiter().GetResult().Accepted), ("generated cache-aside reuses product catalog reads", CacheAsideHits(productCacheAside.Service)), ("generated rate limit rejects product search overflow", RateLimitRejects(productRateLimit.Service)), - ("generated external configuration store loads tenant settings", externalConfiguration.Service.LoadAsync().AsTask().GetAwaiter().GetResult().Loaded) + ("generated external configuration store loads tenant settings", externalConfiguration.Service.LoadAsync().AsTask().GetAwaiter().GetResult().Loaded), + ("notification identity service approves reachable users", identityReady), + ("notification presence service permits delivery", presenceReady), + ("notification rate limiter permits every channel", allChannelsRateLimited), + ("notification preferences include every registered channel", preferredChannels.SequenceEqual([Channel.Push, Channel.Email, Channel.Sms])), + ("notification email sender accepts messages", directEmail is { Channel: Channel.Email, Success: true }), + ("notification sms sender accepts messages", directSms is { Channel: Channel.Sms, Success: true }), + ("notification push sender accepts messages", directPush is { Channel: Channel.Push, Success: true }), + ("notification im sender accepts messages", directIm is { Channel: Channel.Im, Success: true }) ]; } diff --git a/test/PatternKit.Examples.Tests/DomainEventDemo/OrderDomainEventDemoTests.cs b/test/PatternKit.Examples.Tests/DomainEventDemo/OrderDomainEventDemoTests.cs index 0438accd..94c07d93 100644 --- a/test/PatternKit.Examples.Tests/DomainEventDemo/OrderDomainEventDemoTests.cs +++ b/test/PatternKit.Examples.Tests/DomainEventDemo/OrderDomainEventDemoTests.cs @@ -52,4 +52,41 @@ public Task Order_Domain_Event_Demo_Is_Importable_Through_IServiceCollection() ScenarioExpect.Equal("placed:order-300:50", ScenarioExpect.Single(summary.AuditEntries)); }) .AssertPassed(); + + [Scenario("Order Domain Event demo dispatches billed events")] + [Fact] + public Task Order_Domain_Event_Demo_Dispatches_Billed_Events() + => Given("fluent and generated order domain event dispatchers", () => + { + var fluentProjection = new OrderEventProjection(); + var fluentAudit = new List(); + GeneratedOrderDomainEvents.Projection = new OrderEventProjection(); + GeneratedOrderDomainEvents.Audit = []; + + return new BilledEventDispatchers( + OrderDomainEventPolicies.CreateFluentDispatcher(fluentProjection, fluentAudit), + fluentAudit, + GeneratedOrderDomainEvents.CreateDispatcher()); + }) + .When("order billed events are dispatched", (Func FluentAudit, IReadOnlyList GeneratedAudit)>>)(async dispatchers => + { + var fluentEvent = new OrderBilled(Guid.NewGuid(), DateTimeOffset.UtcNow, "order-400", 25m); + var generatedEvent = new OrderBilled(Guid.NewGuid(), DateTimeOffset.UtcNow, "order-500", 30m); + + await dispatchers.Fluent.DispatchAsync(fluentEvent); + await dispatchers.Generated.DispatchAsync(generatedEvent); + + return (dispatchers.FluentAudit.ToArray(), GeneratedOrderDomainEvents.Audit.ToArray()); + })) + .Then("both paths audit billed events", result => + { + ScenarioExpect.Equal("billed:order-400:25", ScenarioExpect.Single(result.FluentAudit)); + ScenarioExpect.Equal("billed:order-500:30", ScenarioExpect.Single(result.GeneratedAudit)); + }) + .AssertPassed(); + + private sealed record BilledEventDispatchers( + PatternKit.Application.DomainEvents.DomainEventDispatcher Fluent, + List FluentAudit, + PatternKit.Application.DomainEvents.IDomainEventDispatcher Generated); } diff --git a/test/PatternKit.Examples.Tests/EnterpriseDemo/EnterpriseOrderDemoTests.cs b/test/PatternKit.Examples.Tests/EnterpriseDemo/EnterpriseOrderDemoTests.cs index 8d8c91c5..436adb6d 100644 --- a/test/PatternKit.Examples.Tests/EnterpriseDemo/EnterpriseOrderDemoTests.cs +++ b/test/PatternKit.Examples.Tests/EnterpriseDemo/EnterpriseOrderDemoTests.cs @@ -139,6 +139,21 @@ public void EUFraudDetector_Always_Passes() ScenarioExpect.False(detector.IsFraudulent(order)); } + [Scenario("AsiaFraudDetector Always Passes")] + [Fact] + public void AsiaFraudDetector_Always_Passes() + { + var detector = new AsiaFraudDetector(); + var order = new Order + { + Id = "test", + CustomerId = "cust", + Region = Region.Asia + }; + + ScenarioExpect.False(detector.IsFraudulent(order)); + } + [Scenario("CreateOrderTemplates Creates Standard Order")] [Fact] public void CreateOrderTemplates_Creates_Standard_Order() diff --git a/test/PatternKit.Examples.Tests/GatewayRoutingDemo/ProductGatewayRoutingDemoTests.cs b/test/PatternKit.Examples.Tests/GatewayRoutingDemo/ProductGatewayRoutingDemoTests.cs index b5175a3a..c1ed1937 100644 --- a/test/PatternKit.Examples.Tests/GatewayRoutingDemo/ProductGatewayRoutingDemoTests.cs +++ b/test/PatternKit.Examples.Tests/GatewayRoutingDemo/ProductGatewayRoutingDemoTests.cs @@ -18,12 +18,15 @@ public Task Fluent_Product_Gateway_Routes_Requests_By_Path() .When("requests are routed", router => new { Inventory = router.Route(new ProductGatewayRequest("/inventory/SKU-100", "tenant-a")), + Pricing = router.Route(new ProductGatewayRequest("/pricing/SKU-100", "tenant-a")), Fallback = router.Route(new ProductGatewayRequest("/unknown/SKU-100", "tenant-a")) }) .Then("matching traffic uses downstream APIs and unknown traffic uses fallback", result => { ScenarioExpect.Equal("inventory", result.Inventory.RouteName); ScenarioExpect.Equal("inventory", result.Inventory.Response.Source); + ScenarioExpect.Equal("pricing", result.Pricing.RouteName); + ScenarioExpect.Equal("pricing", result.Pricing.Response.Source); ScenarioExpect.True(result.Fallback.Fallback); ScenarioExpect.Equal("fallback", result.Fallback.Response.Source); }) diff --git a/test/PatternKit.Examples.Tests/Generators/FacadeSpecsTests.cs b/test/PatternKit.Examples.Tests/Generators/FacadeSpecsTests.cs index 547958a3..e0dc96e9 100644 --- a/test/PatternKit.Examples.Tests/Generators/FacadeSpecsTests.cs +++ b/test/PatternKit.Examples.Tests/Generators/FacadeSpecsTests.cs @@ -74,6 +74,24 @@ public void ShippingFacade_EstimatesDeliveryCorrectly() ScenarioExpect.True(days > 0 && days <= 12); } + [Scenario("ShippingSubsystems CoverFallbackDestinationsAndSpeeds")] + [Fact] + public void ShippingSubsystems_CoverFallbackDestinationsAndSpeeds() + { + var rates = new RateCalculator(); + var estimator = new DeliveryEstimator(); + var facade = new ShippingFacade(estimator, rates, new ShippingValidator()); + + var quote = facade.GetQuote(destination: "international", weight: 8m, speed: "economy"); + var invalidQuote = facade.GetQuote(destination: "", weight: 8m, speed: "economy"); + + ScenarioExpect.Equal(29.99m, rates.CalculateBaseRate("international")); + ScenarioExpect.Equal(1.50m, rates.CalculateWeightSurcharge(8m)); + ScenarioExpect.Equal(12, estimator.EstimateDays("international", "economy")); + ScenarioExpect.Equal("$31.49 - Delivery in 12 business days", quote); + ScenarioExpect.Equal("Invalid shipment parameters", invalidQuote); + } + [Scenario("BillingFacade ProcessesPaymentCorrectly")] [Fact] public void BillingFacade_ProcessesPaymentCorrectly() @@ -219,6 +237,21 @@ public void BillingSubsystems_CoverValidationAndMissingRecordPaths() ScenarioExpect.Equal("Refund amount exceeds original payment", excessiveRefund.ErrorMessage); } + [Scenario("BillingSubsystems CoverSuccessfulRefundPath")] + [Fact] + public void BillingSubsystems_CoverSuccessfulRefundPath() + { + var payments = new PaymentProcessor(); + + var paid = payments.ProcessPayment("CUST001", 25m, "CreditCard"); + var refund = payments.RefundPayment(paid.ReceiptNumber!, 10m); + + ScenarioExpect.True(refund.Success); + ScenarioExpect.NotNull(refund.RefundId); + ScenarioExpect.Equal(10m, refund.RefundedAmount); + ScenarioExpect.NotNull(refund.ProcessedDate); + } + [Scenario("BillingFacade HandlesPaymentAndRefundFailures")] [Fact] public void BillingFacade_HandlesPaymentAndRefundFailures() diff --git a/test/PatternKit.Examples.Tests/InterpreterDemo/InterpreterDemoTests.cs b/test/PatternKit.Examples.Tests/InterpreterDemo/InterpreterDemoTests.cs index 17f18a94..256deeb0 100644 --- a/test/PatternKit.Examples.Tests/InterpreterDemo/InterpreterDemoTests.cs +++ b/test/PatternKit.Examples.Tests/InterpreterDemo/InterpreterDemoTests.cs @@ -184,6 +184,61 @@ public void CreateGeneratedPricingInterpreter_Covers_Production_Pricing_Rules() ctx)); } + [Scenario("CreateGeneratedPricingInterpreter Covers Remaining Generated Operators")] + [Fact] + public void CreateGeneratedPricingInterpreter_Covers_Remaining_Generated_Operators() + { + var interpreter = CreateGeneratedPricingInterpreter(); + var ctx = new PricingContext + { + CartTotal = 80m, + CustomerTier = "Standard" + }; + var percentRule = Expr.NonTerminal("mul", + Expr.Terminal("var", "cart_total"), + Expr.Terminal("percent", "12.5%")); + var comparisonRule = Expr.NonTerminal("add", + Expr.NonTerminal("gt", Expr.Terminal("number", "5"), Expr.Terminal("number", "2")), + Expr.NonTerminal("min", Expr.Terminal("number", "10"), Expr.Terminal("number", "3"))); + var maxRule = Expr.NonTerminal("max", + Expr.Terminal("var", "tier_discount"), + Expr.Terminal("number", "2")); + + ScenarioExpect.Equal(10m, interpreter.Interpret(percentRule, ctx)); + ScenarioExpect.Equal(4m, interpreter.Interpret(comparisonRule, ctx)); + ScenarioExpect.Equal(2m, interpreter.Interpret(maxRule, ctx)); + ScenarioExpect.Equal(0m, interpreter.Interpret(Expr.Terminal("var", "tier_discount"), ctx)); + } + + [Scenario("CreatePricingInterpreter Covers Production Arithmetic And Fallback Rules")] + [Fact] + public void CreatePricingInterpreter_Covers_Production_Arithmetic_And_Fallback_Rules() + { + var interpreter = CreatePricingInterpreter(); + var ctx = new PricingContext + { + CartTotal = 120m, + ItemCount = 4, + CustomerTier = "Standard" + }; + var arithmetic = Expr.NonTerminal("max", + Expr.NonTerminal("sub", Expr.Terminal("number", "20"), Expr.Terminal("number", "5")), + Expr.NonTerminal("div", Expr.Terminal("number", "10"), Expr.Terminal("number", "2"))); + var comparisons = Expr.NonTerminal("add", + Expr.NonTerminal("lt", Expr.Terminal("number", "2"), Expr.Terminal("number", "3")), + Expr.NonTerminal("eq", Expr.Terminal("number", "4"), Expr.Terminal("number", "4"))); + + ScenarioExpect.Equal(0m, interpreter.Interpret(Expr.Terminal("var", "tier_discount"), ctx)); + ScenarioExpect.Equal(15m, interpreter.Interpret(arithmetic, ctx)); + ScenarioExpect.Equal(2m, interpreter.Interpret(comparisons, ctx)); + ScenarioExpect.Equal(0m, interpreter.Interpret( + Expr.NonTerminal("div", Expr.Terminal("number", "9"), Expr.Terminal("number", "0")), + ctx)); + ScenarioExpect.Throws(() => interpreter.Interpret( + Expr.NonTerminal("if", Expr.Terminal("number", "1")), + ctx)); + } + [Scenario("ThresholdDiscountRule Below Threshold")] [Fact] public void ThresholdDiscountRule_Below_Threshold() @@ -323,6 +378,29 @@ public void CreateGeneratedEligibilityInterpreter_Covers_Production_Eligibility_ ScenarioExpect.False(interpreter.Interpret(Expr.Terminal("promo", "WINTER"), ctx)); } + [Scenario("CreateEligibilityInterpreter Covers Production Eligibility Terminals")] + [Fact] + public void CreateEligibilityInterpreter_Covers_Production_Eligibility_Terminals() + { + var interpreter = CreateEligibilityInterpreter(); + var ctx = new PricingContext + { + CartTotal = 120m, + ItemCount = 6, + CustomerTier = "Gold", + PromoCode = "SPRING", + IsHoliday = true + }; + var rule = Expr.NonTerminal("and", + Expr.NonTerminal("or", Expr.Terminal("promo", "SPRING"), Expr.Terminal("bool", "false")), + Expr.NonTerminal("and", Expr.Terminal("isHoliday", ""), Expr.Terminal("itemsOver", "5"))); + + ScenarioExpect.True(interpreter.Interpret(rule, ctx)); + ScenarioExpect.False(interpreter.Interpret(Expr.NonTerminal("not", Expr.Terminal("tier", "Gold")), ctx)); + ScenarioExpect.False(interpreter.Interpret(Expr.Terminal("itemsOver", "10"), ctx)); + ScenarioExpect.False(interpreter.Interpret(Expr.Terminal("promo", "WINTER"), ctx)); + } + [Scenario("VipEligibilityRule Standard High Cart")] [Fact] public void VipEligibilityRule_Standard_High_Cart() @@ -380,6 +458,26 @@ public async Task CreateAsyncPricingInterpreter_Unknown_PromoCode() ScenarioExpect.Equal(0m, result); } + [Scenario("CreateAsyncPricingInterpreter Covers Production Terminal Variants")] + [Fact] + public async Task CreateAsyncPricingInterpreter_Covers_Production_Terminal_Variants() + { + var interpreter = CreateAsyncPricingInterpreter(); + var ctx = new PricingContext { CartTotal = 200m, ItemCount = 3 }; + var saveRule = Expr.NonTerminal("round", + Expr.NonTerminal("mul", Expr.Terminal("var", "cart_total"), Expr.Terminal("promo", "SAVE10"))); + var vipRule = Expr.NonTerminal("max", + Expr.Terminal("percent", "15%"), + Expr.Terminal("promo", "VIP50")); + var itemRule = Expr.NonTerminal("mul", + Expr.Terminal("var", "item_count"), + Expr.Terminal("number", "2")); + + ScenarioExpect.Equal(20m, await interpreter.InterpretAsync(saveRule, ctx)); + ScenarioExpect.Equal(0.50m, await interpreter.InterpretAsync(vipRule, ctx)); + ScenarioExpect.Equal(6m, await interpreter.InterpretAsync(itemRule, ctx)); + } + [Scenario("RunAsync Executes Without Errors")] [Fact] public async Task RunAsync_Executes_Without_Errors() diff --git a/test/PatternKit.Examples.Tests/Pricing/PricingDemoTests.cs b/test/PatternKit.Examples.Tests/Pricing/PricingDemoTests.cs index a29320bb..570e2f09 100644 --- a/test/PatternKit.Examples.Tests/Pricing/PricingDemoTests.cs +++ b/test/PatternKit.Examples.Tests/Pricing/PricingDemoTests.cs @@ -72,6 +72,24 @@ await Given("the public pricing demo sample", () => (Func Given("default pricing artifacts", PricingDemo.BuildDefault) + .Then("all pipeline collaborators are available to importing applications", artifacts => + { + ScenarioExpect.NotNull(artifacts.Pipeline); + ScenarioExpect.NotNull(artifacts.Sources); + ScenarioExpect.NotNull(artifacts.Db); + ScenarioExpect.NotNull(artifacts.Api); + ScenarioExpect.NotNull(artifacts.File); + ScenarioExpect.Equal(3, artifacts.Loyalty.Length); + ScenarioExpect.NotNull(artifacts.Taxes); + ScenarioExpect.Equal(2, artifacts.Rounding.Length); + ScenarioExpect.Equal(3, artifacts.PayDiscounts.Count); + }) + .AssertPassed(); + [Scenario("Source routing: api and file sources resolve when tagged")] [Fact] public async Task SourceRouting_Api_File()