コード例 #1
0
 public override ValueTask UpdateLease(
     OrderQuote responseOrderQuote,
     StoreBookingFlowContext flowContext,
     OrderStateContext stateContext,
     OrderTransaction databaseTransaction)
 {
     // Does nothing at the moment
     return(new ValueTask());
 }
コード例 #2
0
        private bool ReconciliationMismatch(StoreBookingFlowContext flowContext)
        {
            // MissingPaymentDetailsError is handled by OpenActive.Server.NET, so ignoring empty payment details here allows the exception to be thrown by the booking engine.
            if (flowContext.Payment == null)
            {
                return(false);
            }

            return(flowContext.Payment.AccountId != _appSettings.Payment.AccountId || flowContext.Payment.PaymentProviderId != _appSettings.Payment.PaymentProviderId);
        }
コード例 #3
0
        public override void CreateOrder(Order responseOrder, StoreBookingFlowContext flowContext, OrderStateContext stateContext, OrderTransaction databaseTransaction)
        {
            var result = databaseTransaction.Database.AddOrder(
                flowContext.OrderId.ClientId,
                flowContext.OrderId.uuid,
                flowContext.BrokerRole == BrokerType.AgentBroker ? BrokerRole.AgentBroker : flowContext.BrokerRole == BrokerType.ResellerBroker ? BrokerRole.ResellerBroker : BrokerRole.NoBroker,
                flowContext.Broker.Name,
                flowContext.SellerId.SellerIdLong ?? null, // Small hack to allow use of FakeDatabase when in Single Seller mode
                flowContext.Customer.Email,
                flowContext.Payment?.Identifier,
                responseOrder.TotalPaymentDue.Price.Value,
                databaseTransaction.Transaction);

            if (!result)
            {
                throw new OpenBookingException(new OrderAlreadyExistsError());
            }
        }
コード例 #4
0
        public override async ValueTask <Lease> CreateLease(
            OrderQuote responseOrderQuote,
            StoreBookingFlowContext flowContext,
            OrderStateContext stateContext,
            OrderTransaction databaseTransaction)
        {
            if (_appSettings.FeatureFlags.PaymentReconciliationDetailValidation && ReconciliationMismatch(flowContext))
            {
                throw new OpenBookingException(new InvalidPaymentDetailsError(), "Payment reconciliation details do not match");
            }

            // Note if no lease support, simply return null always here instead
            if (flowContext.Stage != FlowStage.C1 && flowContext.Stage != FlowStage.C2)
            {
                return(null);
            }

            // TODO: Make the lease duration configurable
            var leaseExpires = DateTimeOffset.UtcNow + new TimeSpan(0, 5, 0);
            var brokerRole   = BrokerTypeToBrokerRole(flowContext.BrokerRole ?? BrokerType.NoBroker);

            var result = await FakeDatabase.AddLease(
                flowContext.OrderId.ClientId,
                flowContext.OrderId.uuid,
                brokerRole,
                flowContext.Broker.Name,
                flowContext.Broker.Url,
                flowContext.Broker.Telephone,
                flowContext.SellerId.SellerIdLong ?? null, // Small hack to allow use of FakeDatabase when in Single Seller mode
                flowContext.Customer?.Email,
                leaseExpires,
                databaseTransaction.FakeDatabaseTransaction);

            if (!result)
            {
                throw new OpenBookingException(new OrderAlreadyExistsError());
            }

            return(new Lease
            {
                LeaseExpires = leaseExpires
            });
        }
コード例 #5
0
        public override async ValueTask CreateOrder(Order responseOrder, StoreBookingFlowContext flowContext, OrderStateContext stateContext, OrderTransaction databaseTransaction)
        {
            if (_appSettings.FeatureFlags.PaymentReconciliationDetailValidation && responseOrder.TotalPaymentDue.Price > 0 && ReconciliationMismatch(flowContext))
            {
                throw new OpenBookingException(new InvalidPaymentDetailsError(), "Payment reconciliation details do not match");
            }

            var customerType = flowContext.Customer == null ? CustomerType.None : (flowContext.Customer.IsOrganization ? CustomerType.Organization : CustomerType.Person);
            var result       = await FakeDatabase.AddOrder(
                flowContext.OrderId.ClientId,
                flowContext.OrderId.uuid,
                flowContext.BrokerRole == BrokerType.AgentBroker?BrokerRole.AgentBroker : flowContext.BrokerRole == BrokerType.ResellerBroker?BrokerRole.ResellerBroker : BrokerRole.NoBroker,
                flowContext.Broker.Name,
                flowContext.Broker.Url,
                flowContext.Broker.Telephone,
                flowContext.SellerId.SellerIdLong ?? null, // Small hack to allow use of FakeDatabase when in Single Seller mode
                customerType == CustomerType.None?null : flowContext.Customer.Email,
                customerType,
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? flowContext.Customer.Name : null),
                customerType == CustomerType.None?null : flowContext.Customer.Identifier.HasValue?flowContext.Customer.Identifier.Value.ToString() : null,
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? null : flowContext.Customer.GivenName),
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? null : flowContext.Customer.FamilyName),
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? null : flowContext.Customer.Telephone),
                flowContext.Payment?.Identifier,
                flowContext.Payment?.Name,
                flowContext.Payment?.PaymentProviderId,
                flowContext.Payment?.AccountId,
                responseOrder.TotalPaymentDue.Price.Value,
                databaseTransaction.FakeDatabaseTransaction,
                null,
                null);

            if (!result)
            {
                throw new OpenBookingException(new OrderAlreadyExistsError());
            }
        }
コード例 #6
0
        public override Lease CreateLease(OrderQuote responseOrderQuote, StoreBookingFlowContext flowContext, OrderStateContext stateContext, OrderTransaction databaseTransaction)
        {
            // Note if no lease support, simply return null always here instead

            // In this example leasing is only supported at C2
            if (flowContext.Stage == FlowStage.C2)
            {
                // TODO: Make the lease duration configurable
                var leaseExpires = DateTimeOffset.UtcNow + new TimeSpan(0, 5, 0);

                var result = databaseTransaction.Database.AddLease(
                    flowContext.OrderId.ClientId,
                    flowContext.OrderId.uuid,
                    flowContext.BrokerRole == BrokerType.AgentBroker ? BrokerRole.AgentBroker : flowContext.BrokerRole == BrokerType.ResellerBroker ? BrokerRole.ResellerBroker : BrokerRole.NoBroker,
                    flowContext.Broker.Name,
                    flowContext.SellerId.SellerIdLong ?? null, // Small hack to allow use of FakeDatabase when in Single Seller mode
                    flowContext.Customer.Email,
                    leaseExpires,
                    databaseTransaction?.Transaction
                    );

                if (!result)
                {
                    throw new OpenBookingException(new OrderAlreadyExistsError());
                }

                return(new Lease
                {
                    LeaseExpires = leaseExpires
                });
            }
            else
            {
                return(null);
            }
        }
コード例 #7
0
        public override async ValueTask <(Guid, OrderProposalStatus)> CreateOrderProposal(OrderProposal responseOrderProposal, StoreBookingFlowContext flowContext, OrderStateContext stateContext, OrderTransaction databaseTransaction)
        {
            if (!responseOrderProposal.TotalPaymentDue.Price.HasValue)
            {
                throw new OpenBookingException(new OpenBookingError(), "Price must be set on TotalPaymentDue");
            }

            var version      = Guid.NewGuid();
            var customerType = flowContext.Customer == null ? CustomerType.None : (flowContext.Customer.GetType() == typeof(Organization) ? CustomerType.Organization : CustomerType.Person);
            var result       = await FakeDatabase.AddOrder(
                flowContext.OrderId.ClientId,
                flowContext.OrderId.uuid,
                flowContext.BrokerRole == BrokerType.AgentBroker?BrokerRole.AgentBroker : flowContext.BrokerRole == BrokerType.ResellerBroker?BrokerRole.ResellerBroker : BrokerRole.NoBroker,
                flowContext.Broker.Name,
                flowContext.Broker.Url,
                flowContext.Broker.Telephone,
                flowContext.SellerId.SellerIdLong ?? null, // Small hack to allow use of FakeDatabase when in Single Seller mode
                customerType == CustomerType.None?null : flowContext.Customer.Email,
                customerType,
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? flowContext.Customer.Name : null),
                customerType == CustomerType.None?null : flowContext.Customer.Identifier.HasValue?flowContext.Customer.Identifier.Value.ToString() : null,
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? null : flowContext.Customer.GivenName),
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? null : flowContext.Customer.FamilyName),
                customerType == CustomerType.None?null : (customerType == CustomerType.Organization ? null : flowContext.Customer.Telephone),
                flowContext.Payment?.Identifier,
                flowContext.Payment?.Name,
                flowContext.Payment?.PaymentProviderId,
                flowContext.Payment?.AccountId,
                responseOrderProposal.TotalPaymentDue.Price.Value,
                databaseTransaction.FakeDatabaseTransaction,
                version,
                ProposalStatus.AwaitingSellerConfirmation);

            if (!result)
            {
                throw new OpenBookingException(new OrderAlreadyExistsError());
            }

            return(version, OrderProposalStatus.AwaitingSellerConfirmation);
        }
コード例 #8
0
 public override ValueTask <OrderStateContext> Initialise(StoreBookingFlowContext flowContext)
 {
     // Runs before the flow starts, for both leasing and booking
     // Useful for transferring state between stages of the flow
     return(new ValueTask <OrderStateContext>(new OrderStateContext()));
 }
コード例 #9
0
        // TODO check logic here, it's just been copied from BookOrderItems. Possibly could remove duplication here.
        protected override async ValueTask ProposeOrderItems(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 proposal");
                }

                // 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(),
                    true
                    );

                switch (result)
                {
                case ReserveOrderItemsResult.Success:
                    foreach (var(ctx, bookedOrderItemInfo) in ctxGroup.Zip(bookedOrderItemInfos, (ctx, bookedOrderItemInfo) => (ctx, bookedOrderItemInfo)))
                    {
                        ctx.SetOrderItemId(flowContext, bookedOrderItemInfo.OrderItemId);
                    }
                    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");
                }
            }
        }
コード例 #10
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");
                }
            }
        }
コード例 #11
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);
                    }
                }
            }
        }
コード例 #12
0
 public override void UpdateOrder(Order responseOrder, StoreBookingFlowContext flowContext, OrderStateContext stateContext, OrderTransaction databaseTransaction)
 {
     // Runs after the transaction is committed
 }
コード例 #13
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 void GetOrderItem(List <OrderItemContext <SessionOpportunity> > 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 = (from orderItemContext in orderItemContexts
                         join occurances in FakeBookingSystem.Database.Occurrences on orderItemContext.RequestBookableOpportunityOfferId.ScheduledSessionId equals occurances.Id
                         join classes in FakeBookingSystem.Database.Classes on occurances.ClassId equals classes.Id
                         // and offers.id = opportunityOfferId.OfferId
                         select occurances == null ? null : new OrderItem
            {
                AllowCustomerCancellationFullRefund = true,
                // TODO: The static example below should come from the database (which doesn't currently support tax)
                UnitTaxSpecification = flowContext.TaxPayeeRelationship == TaxPayeeRelationship.BusinessToConsumer ?
                                       new List <TaxChargeSpecification>
                {
                    new TaxChargeSpecification
                    {
                        Name = "VAT at 20%",
                        Price = classes.Price * (decimal?)0.2,
                        PriceCurrency = "GBP",
                        Rate = (decimal?)0.2
                    }
                } : null,
                AcceptedOffer = new Offer
                {
                    // Note this should always use RenderOfferId with the supplied SessionOpportunity, to take into account inheritance and OfferType
                    Id = this.RenderOfferId(orderItemContext.RequestBookableOpportunityOfferId),
                    Price = classes.Price,
                    PriceCurrency = "GBP"
                },
                OrderedItem = new ScheduledSession
                {
                    // Note this should always be driven from the database, with new SessionOpportunity's instantiated
                    Id = this.RenderOpportunityId(new SessionOpportunity
                    {
                        OpportunityType = OpportunityType.ScheduledSession,
                        SessionSeriesId = occurances.ClassId,
                        ScheduledSessionId = occurances.Id
                    }),
                    SuperEvent = new SessionSeries
                    {
                        Id = this.RenderOpportunityId(new SessionOpportunity
                        {
                            OpportunityType = OpportunityType.SessionSeries,
                            SessionSeriesId = occurances.ClassId
                        }),
                        Name = classes.Title,
                        Url = new Uri("https://example.com/events/" + occurances.ClassId),
                        Location = new Place
                        {
                            Name = "Fake fitness studio",
                            Geo = new GeoCoordinates
                            {
                                Latitude = 51.6201M,
                                Longitude = 0.302396M
                            }
                        },
                        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)occurances.Start,
                    EndDate = (DateTimeOffset)occurances.End,
                    MaximumAttendeeCapacity = occurances.TotalSpaces,
                    RemainingAttendeeCapacity = occurances.RemainingSpaces
                }
            });

            // 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);

                    if (item.OrderedItem.RemainingAttendeeCapacity == 0)
                    {
                        ctx.AddError(new OpportunityIsFullError());
                    }
                }
            }

            // Add errors to the response according to the attendee details specified as required in the ResponseOrderItem,
            // and those provided in the requestOrderItem
            orderItemContexts.ForEach(ctx => ctx.ValidateAttendeeDetails());

            // Additional attendee detail validation logic goes here
            // ...
        }
コード例 #14
0
        //TODO: This should reuse code of LeaseOrderItem
        protected override void BookOrderItem(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

            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());
                }

                // Attempt to book for those with the same IDs, which is atomic
                List <long> orderItemIds = databaseTransaction.Database.BookOrderItemsForClassOccurrence(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, this.RenderOpportunityJsonLdType(ctxGroup.Key), this.RenderOpportunityId(ctxGroup.Key).ToString(), this.RenderOfferId(ctxGroup.Key).ToString(), ctxGroup.Count());

                if (orderItemIds != null)
                {
                    // Set OrderItemId for each orderItemContext
                    foreach (var(ctx, id) in ctxGroup.Zip(orderItemIds, (ctx, id) => (ctx, id)))
                    {
                        ctx.SetOrderItemId(flowContext, id);
                    }
                }
                else
                {
                    // Note: A real implementation would not through an error this vague
                    throw new OpenBookingException(new OrderCreationFailedError(), "Booking failed for an unexpected reason");
                }
            }
        }
コード例 #15
0
        protected override void LeaseOrderItem(Lease lease, 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

            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)
                {
                    foreach (var ctx in ctxGroup)
                    {
                        ctx.AddError(new OpportunityIntractableError(), "Opportunity ID and type are as not expected for the store. Likely a configuration issue with the Booking System.");
                    }
                }
                else
                {
                    // Attempt to lease for those with the same IDs, which is atomic
                    bool result = databaseTransaction.Database.LeaseOrderItemsForClassOccurrence(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, ctxGroup.Count());

                    if (!result)
                    {
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new OpportunityIntractableError(), "OrderItem could not be leased for unexpected reasons.");
                        }
                    }
                }
            }
        }
コード例 #16
0
        protected override async ValueTask LeaseOrderItems(Lease lease, 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.Where(ctx => !ctx.HasErrors).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)
                {
                    foreach (var ctx in ctxGroup)
                    {
                        ctx.AddError(new OpportunityIntractableError(), "Opportunity ID and type are as not expected for the store. Likely a configuration issue with the Booking System.");
                    }
                }
                else
                {
                    // Attempt to lease for those with the same IDs, which is atomic
                    var(result, capacityErrors, capacityLeaseErrors) = await FakeDatabase.LeaseOrderItemsForFacilitySlot(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, ctxGroup.Count());

                    switch (result)
                    {
                    case ReserveOrderItemsResult.Success:
                        // Do nothing, no errors to add
                        break;

                    case ReserveOrderItemsResult.SellerIdMismatch:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new SellerMismatchError(), "An OrderItem SellerID did not match");
                        }
                        break;

                    case ReserveOrderItemsResult.OpportunityNotFound:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new UnableToProcessOrderItemError(), "Opportunity not found");
                        }
                        break;

                    case ReserveOrderItemsResult.OpportunityOfferPairNotBookable:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new OpportunityOfferPairNotBookableError(), "Opportunity not bookable");
                        }
                        break;

                    case ReserveOrderItemsResult.NotEnoughCapacity:
                        var contexts = ctxGroup.ToArray();
                        for (var i = contexts.Length - 1; i >= 0; i--)
                        {
                            var ctx = contexts[i];
                            if (capacityErrors > 0)
                            {
                                ctx.AddError(new OpportunityHasInsufficientCapacityError());
                                capacityErrors--;
                            }
                            else if (capacityLeaseErrors > 0)
                            {
                                ctx.AddError(new OpportunityCapacityIsReservedByLeaseError());
                                capacityLeaseErrors--;
                            }
                        }

                        break;

                    default:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new OpportunityIntractableError(), "OrderItem could not be leased for unexpected reasons.");
                        }
                        break;
                    }
                }
            }
        }
コード例 #17
0
        public static void AugmentOrderWithTotals <TOrder>(
            TOrder order, StoreBookingFlowContext context, bool businessToConsumerTaxCalculation, bool businessToBusinessTaxCalculation, bool prepaymentAlwaysRequired)
            where TOrder : Order
        {
            if (order == null)
            {
                throw new ArgumentNullException(nameof(order));
            }

            // Calculate total payment due
            decimal totalPaymentDuePrice    = 0;
            string  totalPaymentDueCurrency = null;
            var     totalPaymentTaxMap      = new Dictionary <string, TaxChargeSpecification>();

            foreach (OrderItem orderedItem in order.OrderedItem)
            {
                // Only items with no errors associated are included in the total price
                if (!(orderedItem.Error?.Count > 0))
                {
                    // Keep track of total price
                    totalPaymentDuePrice += orderedItem.AcceptedOffer.Object.Price ?? 0;

                    // Set currency based on first item
                    if (totalPaymentDueCurrency == null)
                    {
                        totalPaymentDueCurrency = orderedItem.AcceptedOffer.Object.PriceCurrency;
                    }
                    else if (totalPaymentDueCurrency != orderedItem.AcceptedOffer.Object.PriceCurrency)
                    {
                        throw new InternalOpenBookingException(new InternalLibraryConfigurationError(), "All currencies in an Order must match");
                    }

                    // Add the taxes to the map
                    if (orderedItem.UnitTaxSpecification != null)
                    {
                        foreach (TaxChargeSpecification taxChargeSpecification in orderedItem.UnitTaxSpecification)
                        {
                            if (totalPaymentTaxMap.TryGetValue(taxChargeSpecification.Name, out TaxChargeSpecification currentTaxValue))
                            {
                                totalPaymentTaxMap[taxChargeSpecification.Name] = AddTaxes(currentTaxValue, taxChargeSpecification);
                            }
                            else
                            {
                                totalPaymentTaxMap[taxChargeSpecification.Name] = taxChargeSpecification;
                            }
                        }
                    }
                }
            }

            switch (context.TaxPayeeRelationship)
            {
            case TaxPayeeRelationship.BusinessToBusiness when businessToBusinessTaxCalculation:
            case TaxPayeeRelationship.BusinessToConsumer when businessToConsumerTaxCalculation:
                order.TotalPaymentTax = totalPaymentTaxMap.Values.ToListOrNullIfEmpty();
                break;

            case TaxPayeeRelationship.BusinessToBusiness:
            case TaxPayeeRelationship.BusinessToConsumer:
                if (order.OrderedItem.Any(o => o.UnitTaxSpecification != null))
                {
                    throw new OpenBookingException(new InternalLibraryConfigurationError());
                }

                order.TotalPaymentTax        = null;
                order.TaxCalculationExcluded = true;
                break;
            }

            // If we're in Net taxMode, tax must be added to get the total price
            if (order.Seller.Object.TaxMode == TaxMode.TaxNet)
            {
                totalPaymentDuePrice += order.TotalPaymentTax.Sum(x => x.Price ?? 0);
            }

            order.TotalPaymentDue = new PriceSpecification
            {
                Price         = totalPaymentDuePrice,
                PriceCurrency = totalPaymentDueCurrency
            };

            if (prepaymentAlwaysRequired)
            {
                if (order.OrderedItem?.Any(x => x?.AcceptedOffer.Object?.OpenBookingPrepayment != null) == true)
                {
                    throw new InternalOpenBookingException(new InternalLibraryConfigurationError(), "OpenBookingPrepayment must not be assigned in AcceptedOffer if PrepaymentAlwaysRequired is true");
                }
            }
            else
            {
                order.TotalPaymentDue.OpenBookingPrepayment = GetRequiredStatusType(order.OrderedItem);
            }
        }
コード例 #18
0
        protected override async ValueTask LeaseOrderItems(
            Lease lease, 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

            foreach (var ctxGroup in orderItemContexts.Where(ctx => !ctx.HasErrors).GroupBy(x => x.RequestBookableOpportunityOfferId))
            {
                // 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: Get existing lease from database

                // Step 2: Compare items in the existing lease to items in the request

                // Step 3: Add/remove lease items to match the request

                //Dictionary<long, int> existingLease = new Dictionary<long, int>();

                // Check that the Opportunity ID and type are as expected for the store
                if (ctxGroup.Key.OpportunityType != OpportunityType.ScheduledSession || !ctxGroup.Key.ScheduledSessionId.HasValue)
                {
                    foreach (var ctx in ctxGroup)
                    {
                        ctx.AddError(new OpportunityIntractableError(), "Opportunity ID and type are as not expected for the store. Likely a configuration issue with the Booking System.");
                    }
                }
                else
                {
                    //var existingOpportunities = existingLease[ctxGroup.Key.ScheduledSessionId.Value];

                    // Attempt to lease for those with the same IDs, which is atomic
                    var(result, capacityErrors, capacityLeaseErrors) = await FakeDatabase.LeaseOrderItemsForClassOccurrence(
                        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,
                        ctxGroup.Count());

                    switch (result)
                    {
                    case ReserveOrderItemsResult.Success:
                        // Do nothing, no errors to add
                        break;

                    case ReserveOrderItemsResult.SellerIdMismatch:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new SellerMismatchError(), "An OrderItem SellerID did not match");
                        }
                        break;

                    case ReserveOrderItemsResult.OpportunityNotFound:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new UnableToProcessOrderItemError(), "Opportunity not found");
                        }
                        break;

                    case ReserveOrderItemsResult.OpportunityOfferPairNotBookable:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new OpportunityOfferPairNotBookableError(), "Opportunity not bookable");
                        }
                        break;

                    case ReserveOrderItemsResult.NotEnoughCapacity:
                        var contexts = ctxGroup.ToArray();
                        for (var i = contexts.Length - 1; i >= 0; i--)
                        {
                            var ctx = contexts[i];
                            if (capacityErrors > 0)
                            {
                                ctx.AddError(new OpportunityHasInsufficientCapacityError());
                                capacityErrors--;
                            }
                            else if (capacityLeaseErrors > 0)
                            {
                                ctx.AddError(new OpportunityCapacityIsReservedByLeaseError());
                                capacityLeaseErrors--;
                            }
                        }

                        break;

                    default:
                        foreach (var ctx in ctxGroup)
                        {
                            ctx.AddError(new OpportunityIntractableError(), "OrderItem could not be leased for unexpected reasons.");
                        }
                        break;
                    }
                }
            }
        }