//TODO: This should reuse code of LeaseOrderItem protected override async ValueTask BookOrderItems(List <OrderItemContext <FacilityOpportunity> > orderItemContexts, StoreBookingFlowContext flowContext, OrderStateContext stateContext, OrderTransaction databaseTransaction) { // Check that there are no conflicts between the supplied opportunities // Also take into account spaces requested across OrderItems against total spaces in each opportunity foreach (var ctxGroup in orderItemContexts.GroupBy(x => x.RequestBookableOpportunityOfferId)) { // Check that the Opportunity ID and type are as expected for the store if (ctxGroup.Key.OpportunityType != OpportunityType.FacilityUseSlot || !ctxGroup.Key.SlotId.HasValue) { throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity ID and type are as not expected for the FacilityStore, during booking"); } // Attempt to book for those with the same IDs, which is atomic var(result, bookedOrderItemInfos) = await FakeDatabase.BookOrderItemsForFacilitySlot( databaseTransaction.FakeDatabaseTransaction, flowContext.OrderId.ClientId, flowContext.SellerId.SellerIdLong ?? null /* Hack to allow this to work in Single Seller mode too */, flowContext.OrderId.uuid, ctxGroup.Key.SlotId.Value, RenderOpportunityId(ctxGroup.Key), RenderOfferId(ctxGroup.Key), ctxGroup.Count(), false ); switch (result) { case ReserveOrderItemsResult.Success: foreach (var(ctx, bookedOrderItemInfo) in ctxGroup.Zip(bookedOrderItemInfos, (ctx, bookedOrderItemInfo) => (ctx, bookedOrderItemInfo))) { // Set OrderItemId and access properties for each orderItemContext ctx.SetOrderItemId(flowContext, bookedOrderItemInfo.OrderItemId); BookedOrderItemHelper.AddPropertiesToBookedOrderItem(ctx, bookedOrderItemInfo); } break; case ReserveOrderItemsResult.SellerIdMismatch: throw new OpenBookingException(new SellerMismatchError(), "An OrderItem SellerID did not match"); case ReserveOrderItemsResult.OpportunityNotFound: throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity not found"); case ReserveOrderItemsResult.NotEnoughCapacity: throw new OpenBookingException(new OpportunityHasInsufficientCapacityError()); case ReserveOrderItemsResult.OpportunityOfferPairNotBookable: throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity and offer pair were not bookable"); default: throw new OpenBookingException(new OrderCreationFailedError(), "Booking failed for an unexpected reason"); } } }
// Similar to the RPDE logic, this needs to render and return an new hypothetical OrderItem from the database based on the supplied opportunity IDs protected override async Task GetOrderItems(List <OrderItemContext <FacilityOpportunity> > orderItemContexts, StoreBookingFlowContext flowContext, OrderStateContext stateContext) { // Note the implementation of this method must also check that this OrderItem is from the Seller specified by context.SellerIdComponents (this is not required if using a Single Seller) // Additionally this method must check that there are enough spaces in each entry // Response OrderItems must be updated into supplied orderItemContexts (including duplicates for multi-party booking) var query = await Task.WhenAll(orderItemContexts.Select(async orderItemContext => { var getOccurrenceInfoResult = await FakeBookingSystem.Database.GetSlotAndBookedOrderItemInfoBySlotId(flowContext.OrderId.uuid, orderItemContext.RequestBookableOpportunityOfferId.SlotId); var(hasFoundOccurrence, facility, slot, bookedOrderItemInfo) = getOccurrenceInfoResult; if (hasFoundOccurrence == false) { return(null); } var remainingUsesIncludingOtherLeases = await FakeBookingSystem.Database.GetNumberOfOtherLeasesForSlot(flowContext.OrderId.uuid, orderItemContext.RequestBookableOpportunityOfferId.SlotId); return(new { OrderItem = new OrderItem { // TODO: The static example below should come from the database (which doesn't currently support tax) UnitTaxSpecification = GetUnitTaxSpecification(flowContext, slot), AcceptedOffer = new Offer { // Note this should always use RenderOfferId with the supplied SessionFacilityOpportunity, to take into account inheritance and OfferType Id = RenderOfferId(orderItemContext.RequestBookableOpportunityOfferId), Price = slot.Price, PriceCurrency = "GBP", LatestCancellationBeforeStartDate = slot.LatestCancellationBeforeStartDate, OpenBookingPrepayment = slot.Prepayment.Convert(), ValidFromBeforeStartDate = slot.ValidFromBeforeStartDate, AllowCustomerCancellationFullRefund = slot.AllowCustomerCancellationFullRefund, }, OrderedItem = new Slot { // Note this should always be driven from the database, with new FacilityOpportunity's instantiated Id = RenderOpportunityId(new FacilityOpportunity { OpportunityType = OpportunityType.FacilityUseSlot, FacilityUseId = slot.FacilityUseId, SlotId = slot.Id }), FacilityUse = new FacilityUse { Id = RenderOpportunityId(new FacilityOpportunity { OpportunityType = OpportunityType.FacilityUse, FacilityUseId = slot.FacilityUseId }), Name = facility.Name, Url = new Uri("https://example.com/events/" + slot.FacilityUseId), Location = new Place { Name = "Fake fitness studio", Geo = new GeoCoordinates { Latitude = facility.LocationLat, Longitude = facility.LocationLng, } }, Activity = new List <Concept> { new Concept { Id = new Uri("https://openactive.io/activity-list#6bdea630-ad22-4e58-98a3-bca26ee3f1da"), PrefLabel = "Rave Fitness", InScheme = new Uri("https://openactive.io/activity-list") } }, }, StartDate = (DateTimeOffset)slot.Start, EndDate = (DateTimeOffset)slot.End, MaximumUses = slot.MaximumUses, // Exclude current Order from the returned lease count RemainingUses = slot.RemainingUses - remainingUsesIncludingOtherLeases }, Attendee = orderItemContext.RequestOrderItem.Attendee, AttendeeDetailsRequired = slot.RequiresAttendeeValidation ? new List <PropertyEnumeration> { PropertyEnumeration.GivenName, PropertyEnumeration.FamilyName, PropertyEnumeration.Email, PropertyEnumeration.Telephone, } : null, OrderItemIntakeForm = slot.RequiresAdditionalDetails ? PropertyValueSpecificationHelper.HydrateAdditionalDetailsIntoPropertyValueSpecifications(slot.RequiredAdditionalDetails) : null, OrderItemIntakeFormResponse = orderItemContext.RequestOrderItem.OrderItemIntakeFormResponse, }, SellerId = _appSettings.FeatureFlags.SingleSeller ? new SellerIdComponents() : new SellerIdComponents { SellerIdLong = facility.SellerId }, slot.RequiresApproval, BookedOrderItemInfo = bookedOrderItemInfo, slot.RemainingUses }); })); // Add the response OrderItems to the relevant contexts (note that the context must be updated within this method) foreach (var(item, ctx) in query.Zip(orderItemContexts, (item, ctx) => (item, ctx))) { if (item == null) { ctx.SetResponseOrderItemAsSkeleton(); ctx.AddError(new UnknownOpportunityError()); } else { ctx.SetResponseOrderItem(item.OrderItem, item.SellerId, flowContext); if (item.BookedOrderItemInfo != null) { BookedOrderItemHelper.AddPropertiesToBookedOrderItem(ctx, item.BookedOrderItemInfo); } if (item.RequiresApproval) { ctx.SetRequiresApproval(); } if (item.RemainingUses == 0) { ctx.AddError(new OpportunityIsFullError()); } // Add validation errors to the OrderItem if either attendee details or additional details are required but not provided var validationErrors = ctx.ValidateDetails(flowContext.Stage); if (validationErrors.Count > 0) { ctx.AddErrors(validationErrors); } } } }
//TODO: This should reuse code of LeaseOrderItem protected override async ValueTask BookOrderItems(List <OrderItemContext <SessionOpportunity> > orderItemContexts, StoreBookingFlowContext flowContext, OrderStateContext stateContext, OrderTransaction databaseTransaction) { // Check that there are no conflicts between the supplied opportunities // Also take into account spaces requested across OrderItems against total spaces in each opportunity // TODO: ENSURE THAT THIS IS CALLED EVERY TIME BY THE STOREBOOKINGENGINE, EVEN WITH ZERO ITEMS // This will ensure that items can be removed from the Order before the booking is confirmed if all items of that type have been removed from the lease // Step 1: Call lease to ensure items are already leased // Step 2: Set OrderItems to booked // Step 3: Write attendee and orderItemIntakeFormResponse to the OrderItem records, for inclusion in P later foreach (var ctxGroup in orderItemContexts.GroupBy(x => x.RequestBookableOpportunityOfferId)) { // Check that the Opportunity ID and type are as expected for the store if (ctxGroup.Key.OpportunityType != OpportunityType.ScheduledSession || !ctxGroup.Key.ScheduledSessionId.HasValue) { throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity ID and type are as not expected for the SessionStore, during booking"); } // Attempt to book for those with the same IDs, which is atomic var(result, bookedOrderItemInfos) = await FakeDatabase.BookOrderItemsForClassOccurrence( databaseTransaction.FakeDatabaseTransaction, flowContext.OrderId.ClientId, flowContext.SellerId.SellerIdLong ?? null /* Hack to allow this to work in Single Seller mode too */, flowContext.OrderId.uuid, ctxGroup.Key.ScheduledSessionId.Value, RenderOpportunityId(ctxGroup.Key), RenderOfferId(ctxGroup.Key), ctxGroup.Count(), false ); switch (result) { case ReserveOrderItemsResult.Success: foreach (var(ctx, bookedOrderItemInfo) in ctxGroup.Zip(bookedOrderItemInfos, (ctx, bookedOrderItemInfo) => (ctx, bookedOrderItemInfo))) { // Set OrderItemId and access properties for each orderItemContext ctx.SetOrderItemId(flowContext, bookedOrderItemInfo.OrderItemId); BookedOrderItemHelper.AddPropertiesToBookedOrderItem(ctx, bookedOrderItemInfo); } break; case ReserveOrderItemsResult.SellerIdMismatch: throw new OpenBookingException(new SellerMismatchError(), "An OrderItem SellerID did not match"); case ReserveOrderItemsResult.OpportunityNotFound: throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity not found"); case ReserveOrderItemsResult.NotEnoughCapacity: throw new OpenBookingException(new OpportunityHasInsufficientCapacityError()); case ReserveOrderItemsResult.OpportunityOfferPairNotBookable: throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity and offer pair were not bookable"); default: throw new OpenBookingException(new OrderCreationFailedError(), "Booking failed for an unexpected reason"); } } }