コード例 #1
0
        //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");
                }
            }
        }
コード例 #2
0
        // 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);
                    }
                }
            }
        }
コード例 #3
0
        //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");
                }
            }
        }