From aee5259cc5eb160f35feb312c113eb63f1176351 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Thu, 29 Apr 2021 11:47:44 +0100 Subject: [PATCH 1/2] More consistent refactor --- .../Controllers/OpenBookingController.cs | 19 +++++ .../Feeds/OrdersFeed.cs | 75 ++++++++++++++--- .../Settings/EngineConfig.cs | 3 +- .../Controllers/OpenBookingController.cs | 19 +++++ .../Feeds/OrdersFeed.cs | 48 +++++++---- .../Settings/EngineConfig.cs | 2 +- .../DatabaseTables.cs | 9 ++- .../FakeBookingSystem.cs | 38 ++++----- .../CustomBookingEngine.cs | 80 +++++++++++++++---- .../Entrypoint/IBookingEngine.cs | 2 + .../OpenBookingHelper/Rpde/OrdersRpdeBase.cs | 4 +- .../Settings/BookingEngineSettings.cs | 3 +- 12 files changed, 234 insertions(+), 68 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Controllers/OpenBookingController.cs b/Examples/BookingSystem.AspNetCore/Controllers/OpenBookingController.cs index 619583a0..d8521680 100644 --- a/Examples/BookingSystem.AspNetCore/Controllers/OpenBookingController.cs +++ b/Examples/BookingSystem.AspNetCore/Controllers/OpenBookingController.cs @@ -211,6 +211,25 @@ public async Task GetOrdersFeed([FromServices] IBookingEngine boo } } + // GET api/openbooking/order-proposals-rpde + [HttpGet("order-proposals-rpde")] + [Authorize(OpenActiveScopes.OrdersFeed)] + public async Task GetOrderProposalsFeed([FromServices] IBookingEngine bookingEngine, long? afterTimestamp, string afterId, long? afterChangeNumber) + { + try + { + // Note only a subset of these parameters will be supplied when this endpoints is called + // They are all provided here for the bookingEngine to choose the correct endpoint + // The auth token must also be provided from the associated authentication method + string clientId = User.GetAccessTokenOrdersFeedClaim(); + return (await bookingEngine.GetOrderProposalsRPDEPageForFeed(clientId, afterTimestamp, afterId, afterChangeNumber)).GetContentResult(); + } + catch (OpenBookingException obe) + { + return obe.ErrorResponseContent.GetContentResult(); + } + } + // POST api/openbooking/test-interface/datasets/uat-ci/opportunities [HttpPost("test-interface/datasets/{testDatasetIdentifier}/opportunities")] public async Task TestInterfaceDatasetInsert([FromServices] IBookingEngine bookingEngine, string testDatasetIdentifier, [FromBody] string @event) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/OrdersFeed.cs b/Examples/BookingSystem.AspNetCore/Feeds/OrdersFeed.cs index cd1ac1ab..67a5ef90 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/OrdersFeed.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/OrdersFeed.cs @@ -20,15 +20,15 @@ protected async override Task> GetRPDEItems(string clientId, long { var q = db.From() .LeftJoin((orders, items) => orders.OrderId == items.OrderId) - .OrderBy(x => x.Modified) + .OrderBy(x => x.OrderModified) .ThenBy(x => x.OrderId) .Where(x => - x.VisibleInFeed != FeedVisibility.None && x.ClientId == clientId && ( + x.VisibleInOrdersFeed != FeedVisibility.None && x.ClientId == clientId && ( !afterTimestamp.HasValue || - x.Modified > afterTimestamp || - x.Modified == afterTimestamp && + x.OrderModified > afterTimestamp || + x.OrderModified == afterTimestamp && string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && - x.Modified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) + x.OrderModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) .Take(RPDEPageSize); var query = db @@ -43,9 +43,9 @@ protected async override Task> GetRPDEItems(string clientId, long { Kind = RpdeKind.Order, Id = result.OrderTable.OrderId, - Modified = result.OrderTable.Modified, - State = result.OrderTable.Deleted || result.OrderTable.VisibleInFeed == FeedVisibility.Archived ? RpdeState.Deleted : RpdeState.Updated, - Data = result.OrderTable.Deleted || result.OrderTable.VisibleInFeed == FeedVisibility.Archived ? null : + Modified = result.OrderTable.OrderModified, + State = result.OrderTable.Deleted || result.OrderTable.VisibleInOrdersFeed == FeedVisibility.Archived ? RpdeState.Deleted : RpdeState.Updated, + Data = result.OrderTable.Deleted || result.OrderTable.VisibleInOrdersFeed == FeedVisibility.Archived ? null : AcmeOrderStore.RenderOrderFromDatabaseResult(RenderOrderId(result.OrderTable.OrderMode == OrderMode.Proposal ? OrderType.OrderProposal : OrderType.Order, result.OrderTable.OrderId), result.OrderTable, result.OrderItemsTable.Select(orderItem => new OrderItem { @@ -87,8 +87,7 @@ protected async override Task> GetRPDEItems(string clientId, long orderItem.Status == BookingStatus.Confirmed ? OrderItemStatus.OrderItemConfirmed : orderItem.Status == BookingStatus.CustomerCancelled ? OrderItemStatus.CustomerCancelled : orderItem.Status == BookingStatus.SellerCancelled ? OrderItemStatus.SellerCancelled : - orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : - orderItem.Status == BookingStatus.Proposed ? OrderItemStatus.OrderItemProposed : (OrderItemStatus?)null, + orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : (OrderItemStatus?)null, CancellationMessage = orderItem.CancellationMessage, CustomerNotice = orderItem.CustomerNotice, }).ToList() @@ -99,4 +98,60 @@ protected async override Task> GetRPDEItems(string clientId, long } } } + + public class AcmeOrderProposalsFeedRpdeGenerator : OrdersRPDEFeedModifiedTimestampAndID + { + //public override string FeedPath { get; protected set; } = "example path override"; + + protected async override Task> GetRPDEItems(string clientId, long? afterTimestamp, string afterId) + { + using (var db = FakeBookingSystem.Database.Mem.Database.Open()) + { + var q = db.From() + .LeftJoin((orders, items) => orders.OrderId == items.OrderId) + .OrderBy(x => x.OrderProposalModified) + .ThenBy(x => x.OrderId) + .Where(x => + x.VisibleInOrderProposalsFeed != FeedVisibility.None && x.ClientId == clientId && ( + !afterTimestamp.HasValue || + x.OrderProposalModified > afterTimestamp || + x.OrderProposalModified == afterTimestamp && + string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && + x.OrderProposalModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) + .Take(RPDEPageSize); + + var query = db + .SelectMulti(q) + .GroupBy(x => new { x.Item1.OrderId }) + .Select(result => new + { + OrderTable = result.Select(item => new { item.Item1 }).FirstOrDefault()?.Item1, + OrderItemsTable = result.Select(item => new { item.Item2 }).ToList().Select(orderItem => orderItem.Item2).ToList() + }) + .Select(result => new RpdeItem + { + Kind = RpdeKind.Order, + Id = result.OrderTable.OrderId, + Modified = result.OrderTable.OrderProposalModified, + State = result.OrderTable.Deleted || result.OrderTable.VisibleInOrderProposalsFeed == FeedVisibility.Archived ? RpdeState.Deleted : RpdeState.Updated, + Data = result.OrderTable.Deleted || result.OrderTable.VisibleInOrderProposalsFeed == FeedVisibility.Archived ? null : + AcmeOrderStore.RenderOrderFromDatabaseResult(RenderOrderId(result.OrderTable.OrderMode == OrderMode.Proposal ? OrderType.OrderProposal : OrderType.Order, result.OrderTable.OrderId), result.OrderTable, + result.OrderItemsTable.Select(orderItem => new OrderItem + { + Id = RenderOrderItemId(OrderType.Order, result.OrderTable.OrderId, orderItem.Id), + AcceptedOffer = new Offer + { + Id = orderItem.OfferJsonLdId, + Price = orderItem.Price, + PriceCurrency = "GBP" + }, + OrderedItem = orderItem.OpportunityJsonLdId + }).ToList() + ) + }); + + return query.ToList(); + } + } + } } diff --git a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs index 280644e7..43446489 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs @@ -157,7 +157,8 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting "{+BaseUrl}/{OrderType}/{uuid}", "{+BaseUrl}/{OrderType}/{uuid}#/orderedItems/{OrderItemIdLong}"), - OrderFeedGenerator = new AcmeOrdersFeedRpdeGenerator() + OrdersFeedGenerator = new AcmeOrdersFeedRpdeGenerator(), + OrderProposalsFeedGenerator = new AcmeOrderProposalsFeedRpdeGenerator() }, new DatasetSiteGeneratorSettings { diff --git a/Examples/BookingSystem.AspNetFramework/Controllers/OpenBookingController.cs b/Examples/BookingSystem.AspNetFramework/Controllers/OpenBookingController.cs index db7966de..2ff6b93f 100644 --- a/Examples/BookingSystem.AspNetFramework/Controllers/OpenBookingController.cs +++ b/Examples/BookingSystem.AspNetFramework/Controllers/OpenBookingController.cs @@ -191,6 +191,25 @@ public async Task GetOrdersFeed(long? afterTimestamp = (lon } } + // GET api/openbooking/order-proposals-rpde + [HttpGet] + [Route("order-proposals-rpde")] + public async Task GetOrderProposalsFeed(long? afterTimestamp = (long?)null, string afterId = null, long? afterChangeNumber = (long?)null) + { + try + { + // Note only a subset of these parameters will be supplied when this endpoints is called + // They are all provided here for the bookingEngine to choose the correct endpoint + // The auth token must also be provided from the associated authentication method + string clientId = AuthenticationHelper.GetClientIdFromAuth(Request, User); + return (await _bookingEngine.GetOrderProposalsRPDEPageForFeed(clientId, afterTimestamp, afterId, afterChangeNumber)).GetContentResult(); + } + catch (OpenBookingException obe) + { + return obe.ErrorResponseContent.GetContentResult(); + } + } + // POST api/openbooking/test-interface/datasets/uat-ci/opportunities [HttpPost] [Route("test-interface/datasets/{testDatasetIdentifier}/opportunities")] diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs b/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs index cd1ac1ab..084ba477 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs @@ -14,22 +14,38 @@ public class AcmeOrdersFeedRpdeGenerator : OrdersRPDEFeedModifiedTimestampAndID { //public override string FeedPath { get; protected set; } = "example path override"; - protected async override Task> GetRPDEItems(string clientId, long? afterTimestamp, string afterId) + protected async override Task> GetRPDEItems(OrderType feedType, string clientId, long? afterTimestamp, string afterId) { + if (!(feedType == OrderType.Order || feedType == OrderType.OrderProposal)) throw new ArgumentOutOfRangeException(nameof(feedType)); using (var db = FakeBookingSystem.Database.Mem.Database.Open()) { - var q = db.From() - .LeftJoin((orders, items) => orders.OrderId == items.OrderId) - .OrderBy(x => x.Modified) - .ThenBy(x => x.OrderId) - .Where(x => - x.VisibleInFeed != FeedVisibility.None && x.ClientId == clientId && ( - !afterTimestamp.HasValue || - x.Modified > afterTimestamp || - x.Modified == afterTimestamp && - string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && - x.Modified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) - .Take(RPDEPageSize); + // Note two queries are only required when implementing the Approval flow, otherwise only an Orders feed is required + var q = (feedType == OrderType.Order ? + db.From() + .LeftJoin((orders, items) => orders.OrderId == items.OrderId) + .OrderBy(x => x.OrderModified) + .ThenBy(x => x.OrderId) + .Where(x => + x.VisibleInOrdersFeed != FeedVisibility.None && x.ClientId == clientId && ( + !afterTimestamp.HasValue || + x.OrderModified > afterTimestamp || + x.OrderModified == afterTimestamp && + string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && + x.OrderModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) + .Take(RPDEPageSize) + : + db.From() + .LeftJoin((orders, items) => orders.OrderId == items.OrderId) + .OrderBy(x => x.OrderProposalModified) + .ThenBy(x => x.OrderId) + .Where(x => + x.VisibleInOrderProposalsFeed != FeedVisibility.None && x.ClientId == clientId && ( + !afterTimestamp.HasValue || + x.OrderProposalModified > afterTimestamp || + x.OrderProposalModified == afterTimestamp && + string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && + x.OrderProposalModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) + .Take(RPDEPageSize)); var query = db .SelectMulti(q) @@ -43,9 +59,9 @@ protected async override Task> GetRPDEItems(string clientId, long { Kind = RpdeKind.Order, Id = result.OrderTable.OrderId, - Modified = result.OrderTable.Modified, - State = result.OrderTable.Deleted || result.OrderTable.VisibleInFeed == FeedVisibility.Archived ? RpdeState.Deleted : RpdeState.Updated, - Data = result.OrderTable.Deleted || result.OrderTable.VisibleInFeed == FeedVisibility.Archived ? null : + Modified = result.OrderTable.OrderModified, + State = result.OrderTable.Deleted || result.OrderTable.VisibleInOrdersFeed == FeedVisibility.Archived ? RpdeState.Deleted : RpdeState.Updated, + Data = result.OrderTable.Deleted || result.OrderTable.VisibleInOrdersFeed == FeedVisibility.Archived ? null : AcmeOrderStore.RenderOrderFromDatabaseResult(RenderOrderId(result.OrderTable.OrderMode == OrderMode.Proposal ? OrderType.OrderProposal : OrderType.Order, result.OrderTable.OrderId), result.OrderTable, result.OrderItemsTable.Select(orderItem => new OrderItem { diff --git a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs index 280644e7..5b97f923 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs @@ -157,7 +157,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting "{+BaseUrl}/{OrderType}/{uuid}", "{+BaseUrl}/{OrderType}/{uuid}#/orderedItems/{OrderItemIdLong}"), - OrderFeedGenerator = new AcmeOrdersFeedRpdeGenerator() + OrdersFeedGenerator = new AcmeOrdersFeedRpdeGenerator() }, new DatasetSiteGeneratorSettings { diff --git a/Fakes/OpenActive.FakeDatabase.NET/DatabaseTables.cs b/Fakes/OpenActive.FakeDatabase.NET/DatabaseTables.cs index 3a1f3570..8a3d441b 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/DatabaseTables.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/DatabaseTables.cs @@ -97,14 +97,16 @@ public class OrderItemsTable : Table public string MeetingPassword { get; set; } } - [CompositeIndex(nameof(Modified), nameof(OrderId))] + [CompositeIndex(nameof(OrderModified), nameof(OrderId))] + [CompositeIndex(nameof(OrderProposalModified), nameof(OrderId))] public class OrderTable { [PrimaryKey] public string OrderId { get; set; } public bool Deleted { get; set; } - public long Modified { get; set; } = DateTimeOffset.Now.UtcTicks; + public long OrderModified { get; set; } = DateTimeOffset.Now.UtcTicks; + public long OrderProposalModified { get; set; } = DateTimeOffset.Now.UtcTicks; public string ClientId { get; set; } [Reference] @@ -119,7 +121,8 @@ public class OrderTable public decimal TotalOrderPrice { get; set; } public OrderMode OrderMode { get; set; } public DateTime LeaseExpires { get; set; } - public FeedVisibility VisibleInFeed { get; set; } + public FeedVisibility VisibleInOrdersFeed { get; set; } + public FeedVisibility VisibleInOrderProposalsFeed { get; set; } public ProposalStatus? ProposalStatus { get; set; } public string ProposalVersionId { get; set; } } diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index 633c814d..08da4cf1 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -202,7 +202,7 @@ public static bool AddLease(string clientId, string uuid, BrokerRole brokerRole, CustomerEmail = customerEmail, OrderMode = OrderMode.Lease, LeaseExpires = leaseExpires.DateTime, - VisibleInFeed = FeedVisibility.None + VisibleInOrdersFeed = FeedVisibility.None }); return true; } @@ -428,8 +428,8 @@ public bool UpdateAccess(string uuid, bool updateAccessPass = false, bool update } } - order.Modified = DateTimeOffset.Now.UtcTicks; - order.VisibleInFeed = FeedVisibility.Visible; + order.OrderModified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrdersFeed = FeedVisibility.Visible; db.Update(order); return true; @@ -461,8 +461,8 @@ public bool UpdateOpportunityAttendance(string uuid) } } - order.Modified = DateTimeOffset.Now.UtcTicks; - order.VisibleInFeed = FeedVisibility.Visible; + order.OrderModified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrdersFeed = FeedVisibility.Visible; db.Update(order); return true; @@ -492,8 +492,8 @@ public bool AddCustomerNotice(string uuid) } } - order.Modified = DateTimeOffset.Now.UtcTicks; - order.VisibleInFeed = FeedVisibility.Visible; + order.OrderModified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrdersFeed = FeedVisibility.Visible; db.Update(order); return true; @@ -544,7 +544,7 @@ public static bool AddOrder(string clientId, string uuid, BrokerRole brokerRole, PaymentIdentifier = paymentIdentifier, TotalOrderPrice = totalOrderPrice, OrderMode = proposalVersionUuid != null ? OrderMode.Proposal : OrderMode.Booking, - VisibleInFeed = FeedVisibility.None, + VisibleInOrdersFeed = FeedVisibility.None, ProposalVersionId = proposalVersionUuid, ProposalStatus = proposalStatus }); @@ -606,7 +606,7 @@ public FakeDatabaseDeleteOrderResult DeleteOrder(string clientId, string uuid, l } order.Deleted = true; order.CustomerEmail = null; - order.Modified = DateTimeOffset.Now.UtcTicks; + order.OrderModified = DateTimeOffset.Now.UtcTicks; db.Update(order); var occurrenceIds = db.Select(x => x.ClientId == clientId && x.OrderId == order.OrderId && x.OccurrenceId.HasValue).Select(x => x.OccurrenceId.Value).Distinct(); @@ -963,8 +963,8 @@ public bool CancelOrderItems(string clientId, long? sellerId, string uuid, List< (x.Status == BookingStatus.Confirmed || x.Status == BookingStatus.Attended)).Sum(x => x.Price); order.TotalOrderPrice = totalPrice; - order.VisibleInFeed = FeedVisibility.Visible; - order.Modified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrdersFeed = FeedVisibility.Visible; + order.OrderModified = DateTimeOffset.Now.UtcTicks; db.Update(order); // Note an actual implementation would need to handle different opportunity types here @@ -1046,8 +1046,8 @@ public bool ReplaceOrderOpportunity(string uuid) db.Update(orderItem); order.TotalOrderPrice = orderItems.Sum(x => x.Price); ; - order.VisibleInFeed = FeedVisibility.Visible; - order.Modified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrdersFeed = FeedVisibility.Visible; + order.OrderModified = DateTimeOffset.Now.UtcTicks; db.Update(order); // Note an actual implementation would need to handle different opportunity types here @@ -1070,8 +1070,8 @@ public bool AcceptOrderProposal(string uuid) { // Update the status and modified date of the OrderProposal to update the feed order.ProposalStatus = ProposalStatus.SellerAccepted; - order.VisibleInFeed = FeedVisibility.Visible; - order.Modified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrderProposalsFeed = FeedVisibility.Visible; + order.OrderProposalModified = DateTimeOffset.Now.UtcTicks; db.Update(order); } return true; @@ -1118,8 +1118,8 @@ public FakeDatabaseBookOrderProposalResult BookOrderProposal(string clientId, lo if (updatedOrderItems.Count > 0 || order.OrderMode != OrderMode.Booking) { order.OrderMode = OrderMode.Booking; - order.VisibleInFeed = FeedVisibility.Archived; - order.Modified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrderProposalsFeed = FeedVisibility.Archived; + order.OrderProposalModified = DateTimeOffset.Now.UtcTicks; db.Update(order); // Note an actual implementation would need to handle different opportunity types here // Update the number of spaces available as a result of cancellation @@ -1150,8 +1150,8 @@ public bool RejectOrderProposal(string clientId, long? sellerId, string uuid, bo if (order.ProposalStatus != ProposalStatus.CustomerRejected && order.ProposalStatus != ProposalStatus.SellerRejected) { order.ProposalStatus = customerRejected ? ProposalStatus.CustomerRejected : ProposalStatus.SellerRejected; - order.VisibleInFeed = FeedVisibility.Visible; - order.Modified = DateTimeOffset.Now.UtcTicks; + order.VisibleInOrderProposalsFeed = FeedVisibility.Visible; + order.OrderProposalModified = DateTimeOffset.Now.UtcTicks; db.Update(order); // Note an actual implementation would need to handle different opportunity types here // Update the number of spaces available as a result of cancellation diff --git a/OpenActive.Server.NET/CustomBookingEngine/CustomBookingEngine.cs b/OpenActive.Server.NET/CustomBookingEngine/CustomBookingEngine.cs index 9e7f7ee5..f40f1e99 100644 --- a/OpenActive.Server.NET/CustomBookingEngine/CustomBookingEngine.cs +++ b/OpenActive.Server.NET/CustomBookingEngine/CustomBookingEngine.cs @@ -92,8 +92,9 @@ public CustomBookingEngine(BookingEngineSettings settings, Uri openBookingAPIBas } // Note that this library does not currently support custom Orders Feed URLs - var ordersFeedUrl = new Uri(openBookingAPIBaseUrl.ToString() + "/orders-rpde"); - settings.OrderFeedGenerator.SetConfiguration(settings.RPDEPageSize, settings.OrderIdTemplate, settings.SellerIdTemplate, ordersFeedUrl); + var ordersFeedBaseUrl = openBookingAPIBaseUrl; + settings.OrdersFeedGenerator.SetConfiguration(settings.RPDEPageSize, settings.OrderIdTemplate, settings.SellerIdTemplate, ordersFeedBaseUrl, OrderType.Order); + if (settings.OrderProposalsFeedGenerator != null) settings.OrderProposalsFeedGenerator.SetConfiguration(settings.RPDEPageSize, settings.OrderIdTemplate, settings.SellerIdTemplate, ordersFeedBaseUrl, OrderType.OrderProposal); settings.SellerStore.SetConfiguration(settings.SellerIdTemplate); @@ -220,7 +221,6 @@ private async Task RouteOpenDataRPDEPageForFeed(string feedname, long? /// /// Handler for an Orders RPDE endpoint (separate to the open data endpoint for security) - string only version - /// Designed to be used on a single controller method with a "feedname" parameter, /// for uses in situations where the framework does not automatically validate numeric values /// /// Token designating the specific authenticated party for which the feed is intended @@ -231,12 +231,13 @@ private async Task RouteOpenDataRPDEPageForFeed(string feedname, long? public async Task GetOrdersRPDEPageForFeed(string clientId, string afterTimestamp, string afterId, string afterChangeNumber) { return ResponseContent.RpdeResponse( - RenderOrdersRPDEPageForFeed( + (await RenderOrdersRPDEPageForFeed( + OrderType.Order, clientId, RpdeOrderingStrategyRouter.ConvertStringToLongOrThrow(afterTimestamp, nameof(afterTimestamp)), afterId, RpdeOrderingStrategyRouter.ConvertStringToLongOrThrow(afterChangeNumber, nameof(afterChangeNumber)) - ).ToString()); + )).ToString()); } /// @@ -250,28 +251,77 @@ public async Task GetOrdersRPDEPageForFeed(string clientId, str /// public async Task GetOrdersRPDEPageForFeed(string clientId, long? afterTimestamp, string afterId, long? afterChangeNumber) { - return ResponseContent.RpdeResponse((await RenderOrdersRPDEPageForFeed(clientId, afterTimestamp, afterId, afterChangeNumber)).ToString()); + return ResponseContent.RpdeResponse((await RenderOrdersRPDEPageForFeed(OrderType.Order, clientId, afterTimestamp, afterId, afterChangeNumber)).ToString()); + } + + /// + /// Handler for an Order Proposals RPDE endpoint (separate to the open data endpoint for security) - string only version + /// for uses in situations where the framework does not automatically validate numeric values + /// + /// Token designating the specific authenticated party for which the feed is intended + /// The "afterTimestamp" parameter from the URL + /// The "afterId" parameter from the URL + /// The "afterChangeNumber" parameter from the URL + /// + public async Task GetOrderProposalsRPDEPageForFeed(string clientId, string afterTimestamp, string afterId, string afterChangeNumber) + { + return ResponseContent.RpdeResponse( + (await GetOrderProposalsRPDEPageForFeed( + clientId, + RpdeOrderingStrategyRouter.ConvertStringToLongOrThrow(afterTimestamp, nameof(afterTimestamp)), + afterId, + RpdeOrderingStrategyRouter.ConvertStringToLongOrThrow(afterChangeNumber, nameof(afterChangeNumber)) + )).ToString()); + } + + /// + /// Handler for an Order Proposals RPDE endpoint (separate to the open data endpoint for security) + /// For uses in situations where the framework does not automatically validate numeric values + /// + /// Token designating the specific authenticated party for which the feed is intended + /// The "afterTimestamp" parameter from the URL + /// The "afterId" parameter from the URL + /// The "afterChangeNumber" parameter from the URL + /// + public async Task GetOrderProposalsRPDEPageForFeed(string clientId, long? afterTimestamp, string afterId, long? afterChangeNumber) + { + return ResponseContent.RpdeResponse((await RenderOrdersRPDEPageForFeed(OrderType.OrderProposal, clientId, afterTimestamp, afterId, afterChangeNumber)).ToString()); } /// /// Handler for Orders RPDE endpoint /// + /// Type of feed to render /// Token designating the specific authenticated party for which the feed is intended /// The "afterTimestamp" parameter from the URL /// The "afterId" parameter from the URL /// The "afterChangeNumber" parameter from the URL /// - private async Task RenderOrdersRPDEPageForFeed(string clientId, long? afterTimestamp, string afterId, long? afterChangeNumber) + private async Task RenderOrdersRPDEPageForFeed(OrderType feedType, string clientId, long? afterTimestamp, string afterId, long? afterChangeNumber) { - if (settings.OrderFeedGenerator != null) + // Add lookup against clientId and pass this into generator? + switch (feedType) { - // Add lookup against clientId and pass this into generator? - return await settings.OrderFeedGenerator.GetRpdePage(clientId, afterTimestamp, afterId, afterChangeNumber); - } - else - { - // TODO: Change to Not Authorised Error - throw new OpenBookingException(new NotFoundError(), $"Access to this endpoint is not authorised."); + case OrderType.OrderProposal: + if (settings.OrderProposalsFeedGenerator != null) + { + return await settings.OrderProposalsFeedGenerator.GetRpdePage(clientId, afterTimestamp, afterId, afterChangeNumber); + } + else + { + throw new OpenBookingException(new NotFoundError(), "This endpoint is not available in this implementation."); + } + case OrderType.Order: + if (settings.OrdersFeedGenerator != null) + { + return await settings.OrdersFeedGenerator.GetRpdePage(clientId, afterTimestamp, afterId, afterChangeNumber); + } + else + { + throw new OpenBookingException(new NotFoundError(), "This endpoint is not available in this implementation."); + } + default: + throw new ArgumentOutOfRangeException(nameof(feedType)); } } diff --git a/OpenActive.Server.NET/Entrypoint/IBookingEngine.cs b/OpenActive.Server.NET/Entrypoint/IBookingEngine.cs index 6c7a0612..1f8a0eb0 100644 --- a/OpenActive.Server.NET/Entrypoint/IBookingEngine.cs +++ b/OpenActive.Server.NET/Entrypoint/IBookingEngine.cs @@ -38,6 +38,8 @@ public interface IBookingEngine Task TriggerTestAction(string actionJson); Task GetOrdersRPDEPageForFeed(string clientId, string afterTimestamp, string afterId, string afterChangeNumber); Task GetOrdersRPDEPageForFeed(string clientId, long? afterTimestamp, string afterId, long? afterChangeNumber); + Task GetOrderProposalsRPDEPageForFeed(string clientId, string afterTimestamp, string afterId, string afterChangeNumber); + Task GetOrderProposalsRPDEPageForFeed(string clientId, long? afterTimestamp, string afterId, long? afterChangeNumber); Task GetOrderStatus(string clientId, Uri sellerId, string uuid); } } \ No newline at end of file diff --git a/OpenActive.Server.NET/OpenBookingHelper/Rpde/OrdersRpdeBase.cs b/OpenActive.Server.NET/OpenBookingHelper/Rpde/OrdersRpdeBase.cs index d0c49b8c..11cae2c9 100644 --- a/OpenActive.Server.NET/OpenBookingHelper/Rpde/OrdersRpdeBase.cs +++ b/OpenActive.Server.NET/OpenBookingHelper/Rpde/OrdersRpdeBase.cs @@ -14,14 +14,14 @@ public abstract class OrdersRPDEFeedGenerator : OrdersModelSupport, IRpdeFeedGen protected Uri FeedUrl { get; private set; } - internal void SetConfiguration(int rpdePageSize, OrderIdTemplate orderIdTemplate, SingleIdTemplate sellerIdTemplate, Uri offersFeedUrl) + internal void SetConfiguration(int rpdePageSize, OrderIdTemplate orderIdTemplate, SingleIdTemplate sellerIdTemplate, Uri ordersFeedBaseUrl, OrderType feedType) { base.SetConfiguration(orderIdTemplate, sellerIdTemplate); this.RPDEPageSize = rpdePageSize; // Allow these to be overridden by implementations if customisation is required - this.FeedUrl = offersFeedUrl; + this.FeedUrl = new Uri(ordersFeedBaseUrl.ToString() + "/" + (feedType == OrderType.Order ? "orders" : "order-proposals") + "-rpde"); } /// diff --git a/OpenActive.Server.NET/OpenBookingHelper/Settings/BookingEngineSettings.cs b/OpenActive.Server.NET/OpenBookingHelper/Settings/BookingEngineSettings.cs index b4579223..1206fb1f 100644 --- a/OpenActive.Server.NET/OpenBookingHelper/Settings/BookingEngineSettings.cs +++ b/OpenActive.Server.NET/OpenBookingHelper/Settings/BookingEngineSettings.cs @@ -26,7 +26,8 @@ public class BookingEngineSettings public Dictionary OpenDataFeeds { get; set; } public int RPDEPageSize { get; set; } = 500; public Uri JsonLdIdBaseUrl { get; set; } - public OrdersRPDEFeedGenerator OrderFeedGenerator { get; set; } + public OrdersRPDEFeedGenerator OrdersFeedGenerator { get; set; } + public OrdersRPDEFeedGenerator OrderProposalsFeedGenerator { get; set; } public SellerStore SellerStore { get; set; } public bool HasSingleSeller { get; set; } } From 8d0dcb0d5b7e93f8c4be29465e7a675e78b4cb98 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Thu, 29 Apr 2021 12:53:00 +0100 Subject: [PATCH 2/2] support OrderProposals feed --- .../Stores/OrderStore.cs | 3 +- .../Feeds/OrdersFeed.cs | 101 ++++++++++++------ .../Settings/EngineConfig.cs | 3 +- .../Stores/OrderStore.cs | 3 +- .../StoreBookingEngine/StoreBookingEngine.cs | 4 +- 5 files changed, 76 insertions(+), 38 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Stores/OrderStore.cs b/Examples/BookingSystem.AspNetCore/Stores/OrderStore.cs index bd6545b1..571aa58b 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/OrderStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/OrderStore.cs @@ -406,8 +406,7 @@ public async override Task GetOrderStatus(OrderIdComponents orderId, Sell orderItem.Status == BookingStatus.Confirmed ? OrderItemStatus.OrderItemConfirmed : orderItem.Status == BookingStatus.CustomerCancelled ? OrderItemStatus.CustomerCancelled : orderItem.Status == BookingStatus.SellerCancelled ? OrderItemStatus.SellerCancelled : - orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : - orderItem.Status == BookingStatus.Proposed ? OrderItemStatus.OrderItemProposed : (OrderItemStatus?)null + orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : (OrderItemStatus?)null }).ToList(); var order = RenderOrderFromDatabaseResult(orderIdUri, dbOrder, orderItems); diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs b/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs index 084ba477..67a5ef90 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/OrdersFeed.cs @@ -14,38 +14,22 @@ public class AcmeOrdersFeedRpdeGenerator : OrdersRPDEFeedModifiedTimestampAndID { //public override string FeedPath { get; protected set; } = "example path override"; - protected async override Task> GetRPDEItems(OrderType feedType, string clientId, long? afterTimestamp, string afterId) + protected async override Task> GetRPDEItems(string clientId, long? afterTimestamp, string afterId) { - if (!(feedType == OrderType.Order || feedType == OrderType.OrderProposal)) throw new ArgumentOutOfRangeException(nameof(feedType)); using (var db = FakeBookingSystem.Database.Mem.Database.Open()) { - // Note two queries are only required when implementing the Approval flow, otherwise only an Orders feed is required - var q = (feedType == OrderType.Order ? - db.From() - .LeftJoin((orders, items) => orders.OrderId == items.OrderId) - .OrderBy(x => x.OrderModified) - .ThenBy(x => x.OrderId) - .Where(x => - x.VisibleInOrdersFeed != FeedVisibility.None && x.ClientId == clientId && ( - !afterTimestamp.HasValue || - x.OrderModified > afterTimestamp || - x.OrderModified == afterTimestamp && - string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && - x.OrderModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) - .Take(RPDEPageSize) - : - db.From() - .LeftJoin((orders, items) => orders.OrderId == items.OrderId) - .OrderBy(x => x.OrderProposalModified) - .ThenBy(x => x.OrderId) - .Where(x => - x.VisibleInOrderProposalsFeed != FeedVisibility.None && x.ClientId == clientId && ( - !afterTimestamp.HasValue || - x.OrderProposalModified > afterTimestamp || - x.OrderProposalModified == afterTimestamp && - string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && - x.OrderProposalModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) - .Take(RPDEPageSize)); + var q = db.From() + .LeftJoin((orders, items) => orders.OrderId == items.OrderId) + .OrderBy(x => x.OrderModified) + .ThenBy(x => x.OrderId) + .Where(x => + x.VisibleInOrdersFeed != FeedVisibility.None && x.ClientId == clientId && ( + !afterTimestamp.HasValue || + x.OrderModified > afterTimestamp || + x.OrderModified == afterTimestamp && + string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && + x.OrderModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) + .Take(RPDEPageSize); var query = db .SelectMulti(q) @@ -103,8 +87,7 @@ protected async override Task> GetRPDEItems(OrderType feedType, s orderItem.Status == BookingStatus.Confirmed ? OrderItemStatus.OrderItemConfirmed : orderItem.Status == BookingStatus.CustomerCancelled ? OrderItemStatus.CustomerCancelled : orderItem.Status == BookingStatus.SellerCancelled ? OrderItemStatus.SellerCancelled : - orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : - orderItem.Status == BookingStatus.Proposed ? OrderItemStatus.OrderItemProposed : (OrderItemStatus?)null, + orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : (OrderItemStatus?)null, CancellationMessage = orderItem.CancellationMessage, CustomerNotice = orderItem.CustomerNotice, }).ToList() @@ -115,4 +98,60 @@ protected async override Task> GetRPDEItems(OrderType feedType, s } } } + + public class AcmeOrderProposalsFeedRpdeGenerator : OrdersRPDEFeedModifiedTimestampAndID + { + //public override string FeedPath { get; protected set; } = "example path override"; + + protected async override Task> GetRPDEItems(string clientId, long? afterTimestamp, string afterId) + { + using (var db = FakeBookingSystem.Database.Mem.Database.Open()) + { + var q = db.From() + .LeftJoin((orders, items) => orders.OrderId == items.OrderId) + .OrderBy(x => x.OrderProposalModified) + .ThenBy(x => x.OrderId) + .Where(x => + x.VisibleInOrderProposalsFeed != FeedVisibility.None && x.ClientId == clientId && ( + !afterTimestamp.HasValue || + x.OrderProposalModified > afterTimestamp || + x.OrderProposalModified == afterTimestamp && + string.Compare(afterId, x.OrderId, StringComparison.InvariantCulture) > 0) && + x.OrderProposalModified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) + .Take(RPDEPageSize); + + var query = db + .SelectMulti(q) + .GroupBy(x => new { x.Item1.OrderId }) + .Select(result => new + { + OrderTable = result.Select(item => new { item.Item1 }).FirstOrDefault()?.Item1, + OrderItemsTable = result.Select(item => new { item.Item2 }).ToList().Select(orderItem => orderItem.Item2).ToList() + }) + .Select(result => new RpdeItem + { + Kind = RpdeKind.Order, + Id = result.OrderTable.OrderId, + Modified = result.OrderTable.OrderProposalModified, + State = result.OrderTable.Deleted || result.OrderTable.VisibleInOrderProposalsFeed == FeedVisibility.Archived ? RpdeState.Deleted : RpdeState.Updated, + Data = result.OrderTable.Deleted || result.OrderTable.VisibleInOrderProposalsFeed == FeedVisibility.Archived ? null : + AcmeOrderStore.RenderOrderFromDatabaseResult(RenderOrderId(result.OrderTable.OrderMode == OrderMode.Proposal ? OrderType.OrderProposal : OrderType.Order, result.OrderTable.OrderId), result.OrderTable, + result.OrderItemsTable.Select(orderItem => new OrderItem + { + Id = RenderOrderItemId(OrderType.Order, result.OrderTable.OrderId, orderItem.Id), + AcceptedOffer = new Offer + { + Id = orderItem.OfferJsonLdId, + Price = orderItem.Price, + PriceCurrency = "GBP" + }, + OrderedItem = orderItem.OpportunityJsonLdId + }).ToList() + ) + }); + + return query.ToList(); + } + } + } } diff --git a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs index 5b97f923..43446489 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs @@ -157,7 +157,8 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting "{+BaseUrl}/{OrderType}/{uuid}", "{+BaseUrl}/{OrderType}/{uuid}#/orderedItems/{OrderItemIdLong}"), - OrdersFeedGenerator = new AcmeOrdersFeedRpdeGenerator() + OrdersFeedGenerator = new AcmeOrdersFeedRpdeGenerator(), + OrderProposalsFeedGenerator = new AcmeOrderProposalsFeedRpdeGenerator() }, new DatasetSiteGeneratorSettings { diff --git a/Examples/BookingSystem.AspNetFramework/Stores/OrderStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/OrderStore.cs index bd6545b1..571aa58b 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/OrderStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/OrderStore.cs @@ -406,8 +406,7 @@ public async override Task GetOrderStatus(OrderIdComponents orderId, Sell orderItem.Status == BookingStatus.Confirmed ? OrderItemStatus.OrderItemConfirmed : orderItem.Status == BookingStatus.CustomerCancelled ? OrderItemStatus.CustomerCancelled : orderItem.Status == BookingStatus.SellerCancelled ? OrderItemStatus.SellerCancelled : - orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : - orderItem.Status == BookingStatus.Proposed ? OrderItemStatus.OrderItemProposed : (OrderItemStatus?)null + orderItem.Status == BookingStatus.Attended ? OrderItemStatus.CustomerAttended : (OrderItemStatus?)null }).ToList(); var order = RenderOrderFromDatabaseResult(orderIdUri, dbOrder, orderItems); diff --git a/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs b/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs index 057d0d49..a9b44a82 100644 --- a/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs +++ b/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs @@ -586,8 +586,8 @@ public async override Task ProcessFlowRequest(BookingFlowContext throw new OpenBookingException(new InternalLibraryError(), "SetOrderItemId must be called for each OrderItemContext in ProposeOrderItems"); } - // Set the orderItemStatus to be https://openactive.io/OrderItemProposed (as it must always be so in the response of P) - ctx.ResponseOrderItem.OrderItemStatus = OrderItemStatus.OrderItemProposed; + // Set the orderItemStatus to null (as it must always be so in the response of P) + ctx.ResponseOrderItem.OrderItemStatus = null; } }