From ecc3fb6c239d69f9eec7b9da5a118555e81f761b Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Tue, 13 Jun 2023 18:48:38 +0100 Subject: [PATCH 01/14] feat: add IFU functionality --- .../Feeds/FacilitiesFeeds.cs | 33 +- .../IdComponents/FacilityOpportunity.cs | 5 +- .../Properties/launchSettings.json | 9 +- .../Settings/AppSettings.cs | 1 + .../Settings/EngineConfig.cs | 69 +- .../Stores/FacilityStore.cs | 213 +++-- .../appsettings.single-seller.json | 3 +- .../Feeds/FacilitiesFeeds.cs | 33 +- .../IdComponents/FacilityOpportunity.cs | 5 +- .../Settings/AppSettings.cs | 1 + .../Settings/EngineConfig.cs | 69 +- .../Stores/FacilityStore.cs | 213 +++-- .../BookingSystem.AspNetFramework/Web.config | 748 +++++++++--------- .../FakeBookingSystem.cs | 189 +++-- .../Models/FacilityUseTable.cs | 24 + .../Models/SlotTable.cs | 2 + .../IdTransforms/IdTemplate.cs | 2 +- 17 files changed, 986 insertions(+), 633 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index caa10fbb..c5bf8926 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -111,7 +111,17 @@ protected override async Task>> GetRpdeItems(long? af PrefLabel = "Squash Court", InScheme = new Uri("https://openactive.io/facility-types") } - } + }, + IndividualFacilityUse = result.Item1.IndividualFacilityUses != null ? result.Item1.IndividualFacilityUses.Select(ifu => new OpenActive.NET.IndividualFacilityUse + { + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = ifu.Id, + FacilityUseId = result.Item1.Id + }), + Name = ifu.Name + }).ToList() : null, } }); @@ -145,7 +155,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => new RpdeItem { - Kind = RpdeKind.FacilityUseSlot, + Kind = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? RpdeKind.IndividualFacilityUseSlot : RpdeKind.FacilityUseSlot, Id = x.Id, Modified = x.Modified, State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, @@ -156,11 +166,19 @@ protected override async Task>> GetRpdeItems(long? afterTime // constant as power of configuration through underlying class grows (i.e. as new properties are added) Id = RenderOpportunityId(new FacilityOpportunity { - OpportunityType = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, }), - FacilityUse = RenderOpportunityId(new FacilityOpportunity + FacilityUse = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = x.IndividualFacilityUseId, + FacilityUseId = x.FacilityUseId, + }) + : RenderOpportunityId(new FacilityOpportunity { OpportunityType = OpportunityType.FacilityUse, FacilityUseId = x.FacilityUseId @@ -176,9 +194,10 @@ protected override async Task>> GetRpdeItems(long? afterTime Id = RenderOfferId(new FacilityOpportunity { OfferId = 0, - OpportunityType = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, }), Price = x.Price, PriceCurrency = "GBP", diff --git a/Examples/BookingSystem.AspNetCore/IdComponents/FacilityOpportunity.cs b/Examples/BookingSystem.AspNetCore/IdComponents/FacilityOpportunity.cs index ad6e4e7e..5084c467 100644 --- a/Examples/BookingSystem.AspNetCore/IdComponents/FacilityOpportunity.cs +++ b/Examples/BookingSystem.AspNetCore/IdComponents/FacilityOpportunity.cs @@ -17,6 +17,7 @@ public class FacilityOpportunity : IBookableIdComponents public long? FacilityUseId { get; set; } public long? SlotId { get; set; } public long? OfferId { get; set; } + public long? IndividualFacilityUseId { get; set; } public override bool Equals(object obj) { @@ -27,7 +28,8 @@ public override bool Equals(object obj) return OpportunityType == other.OpportunityType && FacilityUseId == other.FacilityUseId && SlotId == other.SlotId && - OfferId == other.OfferId; + OfferId == other.OfferId && + IndividualFacilityUseId == other.IndividualFacilityUseId; } public override int GetHashCode() @@ -39,6 +41,7 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ FacilityUseId.GetHashCode(); hashCode = (hashCode * 397) ^ SlotId.GetHashCode(); hashCode = (hashCode * 397) ^ OfferId.GetHashCode(); + hashCode = (hashCode * 397) ^ IndividualFacilityUseId.GetHashCode(); // ReSharper enable NonReadonlyMemberInGetHashCode return hashCode; } diff --git a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json index 30b0586b..5828de08 100644 --- a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json +++ b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json @@ -1,8 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true - }, "profiles": { "BookingSystem.AspNetCore": { "commandName": "Project", @@ -10,8 +6,9 @@ "launchUrl": "https://localhost:5001/openactive", "applicationUrl": "https://localhost:5001", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ApplicationHostBaseUrl": "https://localhost:5001" + "ASPNETCORE_ENVIRONMENT": "single-seller", + "GENERATE_INDIVIDUAL_FACILITY_USES": "true", + "OPPORTUNITY_COUNT": "200" } } } diff --git a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs index 6ead5201..1cce79c1 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs @@ -18,6 +18,7 @@ public class FeatureSettings public bool PaymentReconciliationDetailValidation { get; set; } = true; public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; + public bool GenerateIndividualFacilityUses { get; set; } = false; } public class PaymentSettings diff --git a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs index 2f01e945..4ae4c03b 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs @@ -11,6 +11,51 @@ public static class EngineConfig { public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSettings) { + var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.IndividualFacilityUseSlot, + AssignedFeed = OpportunityType.IndividualFacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + Bookable = true + }, + // Parent + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.IndividualFacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}" + }, + // Grandparent + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.FacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + : + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.FacilityUseSlot, + AssignedFeed = OpportunityType.FacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + Bookable = true + }, + // Parent + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.FacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + ; + return new StoreBookingEngine( new BookingEngineSettings { @@ -37,24 +82,8 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting Bookable = false }), - new BookablePairIdTemplate ( - // Opportunity - new OpportunityIdConfiguration - { - OpportunityType = OpportunityType.FacilityUseSlot, - AssignedFeed = OpportunityType.FacilityUseSlot, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}", - OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", - Bookable = true - }, - // Parent - new OpportunityIdConfiguration - { - OpportunityType = OpportunityType.FacilityUse, - AssignedFeed = OpportunityType.FacilityUse, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" - })/*, - + facilityBookablePaidIdTemplate, + /* new BookablePairIdTemplate( // Opportunity new OpportunityIdConfiguration @@ -147,7 +176,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OpportunityType.FacilityUse, new AcmeFacilityUseRpdeGenerator(appSettings) }, { - OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings) + appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings) } }, @@ -252,7 +281,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting new SessionStore(appSettings), new List { OpportunityType.ScheduledSession } }, { - new FacilityStore(appSettings), new List { OpportunityType.FacilityUseSlot } + new FacilityStore(appSettings), new List { appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot } } }, OrderStore = new AcmeOrderStore(appSettings), diff --git a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs index ef3e865c..7eba84d5 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs @@ -35,197 +35,218 @@ protected override async Task CreateOpportunityWithinTestDa long? sellerId = _appSettings.FeatureFlags.SingleSeller ? null : seller.IdLong; var requiresApproval = openBookingFlow == TestOpenBookingFlowEnumeration.OpenBookingApprovalFlow; + var generateIndividualFacilityUses = _appSettings.FeatureFlags.GenerateIndividualFacilityUses; switch (opportunityType) { case OpportunityType.FacilityUseSlot: + case OpportunityType.IndividualFacilityUseSlot: int facilityId, slotId; + int? individualFacilityUseId; switch (criteria) { case TestOpportunityCriteriaEnumeration.TestOpportunityBookable: case TestOpportunityCriteriaEnumeration.TestOpportunityOfflineBookable: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility", rnd.Next(2) == 0 ? 0M : 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellable: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFree: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableUsingPayment: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableFree: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility", 0M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithinValidFromBeforeStartDate: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableOutsideValidFromBeforeStartDate: { var isValid = criteria == TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithinValidFromBeforeStartDate; - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, $"[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility {(isValid ? "Within" : "Outside")} Window", 14.99M, 10, requiresApproval, - validFromStartDate: isValid); + validFromStartDate: isValid, + generateIndividualFacilityUses: generateIndividualFacilityUses); } break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellableWithinWindow: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellableOutsideWindow: { var isValid = criteria == TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellableWithinWindow; - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, $"[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility {(isValid ? "Within" : "Outside")} Cancellation Window", 14.99M, 10, requiresApproval, - latestCancellationBeforeStartDate: isValid); + latestCancellationBeforeStartDate: isValid, + generateIndividualFacilityUses: generateIndividualFacilityUses); } break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentOptional: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Prepayment Optional", 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Optional); + prepayment: RequiredStatusType.Optional, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentUnavailable: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Prepayment Unavailable", 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Unavailable); + prepayment: RequiredStatusType.Unavailable, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentRequired: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Prepayment Required", 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Required); + prepayment: RequiredStatusType.Required, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNoSpaces: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility No Spaces", 14.99M, 0, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableFiveSpaces: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility Five Spaces", 14.99M, 5, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableOneSpace: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility One Space", 14.99M, 1, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreeTaxNet: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 2, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Tax Net", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreeTaxGross: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 1, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Tax Gross", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableSellerTermsOfService: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 1, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility With Seller Terms Of Service", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableAttendeeDetails: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 1, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility That Requires Attendee Details", 14.99M, 10, requiresApproval, - requiresAttendeeValidation: true); + requiresAttendeeValidation: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableAdditionalDetails: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility That Requires Additional Details", 10M, 10, requiresApproval, - requiresAdditionalDetails: true); + requiresAdditionalDetails: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithNegotiation: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility That Allows Proposal Amendment", 10M, 10, requiresApproval, - allowProposalAmendment: true); + allowProposalAmendment: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNotCancellable: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility Paid That Does Not Allow Full Refund", 10M, 10, requiresApproval, - allowCustomerCancellationFullRefund: false); + allowCustomerCancellationFullRefund: false, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableInPast: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility in the Past", @@ -233,7 +254,8 @@ protected override async Task CreateOpportunityWithinTestDa 10, requiresApproval, allowCustomerCancellationFullRefund: false, - inPast: true); + inPast: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; default: throw new OpenBookingException(new OpenBookingError(), "testOpportunityCriteria value not supported"); @@ -243,7 +265,8 @@ protected override async Task CreateOpportunityWithinTestDa { OpportunityType = opportunityType, FacilityUseId = facilityId, - SlotId = slotId + SlotId = slotId, + IndividualFacilityUseId = individualFacilityUseId }; default: throw new OpenBookingException(new OpenBookingError(), "Opportunity Type not supported"); @@ -302,6 +325,79 @@ protected override async Task GetOrderItems(List ifu.Id == slot.IndividualFacilityUseId); + slotParent = new OpenActive.NET.IndividualFacilityUse + { + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = individualFacilityUse.Id, + FacilityUseId = facility.Id, + }), + Name = individualFacilityUse.Name, + AggregateFacilityUse = 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, + } + }, + FacilityType = new List { + new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + } + } + } + }; + } + else + { + slotParent = 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, + } + }, + FacilityType = new List { + new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + } + } + }; + } + return new { OrderItem = new OrderItem @@ -324,37 +420,12 @@ protected override async Task GetOrderItems(List { - new Concept - { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", - InScheme = new Uri("https://openactive.io/facility-types") - } - } - }, + FacilityUse = slotParent, StartDate = (DateTimeOffset)slot.Start, EndDate = (DateTimeOffset)slot.End, MaximumUses = slot.MaximumUses, @@ -422,7 +493,7 @@ protected override async ValueTask LeaseOrderItems(Lease lease, List !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) + if ((ctxGroup.Key.OpportunityType != OpportunityType.FacilityUseSlot && ctxGroup.Key.OpportunityType != OpportunityType.IndividualFacilityUseSlot) || !ctxGroup.Key.SlotId.HasValue) { foreach (var ctx in ctxGroup) { @@ -494,7 +565,7 @@ protected override async ValueTask BookOrderItems(List 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) + if ((ctxGroup.Key.OpportunityType != OpportunityType.FacilityUseSlot && ctxGroup.Key.OpportunityType != OpportunityType.IndividualFacilityUseSlot) || !ctxGroup.Key.SlotId.HasValue) { throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity ID and type are as not expected for the FacilityStore, during booking"); } @@ -557,7 +628,7 @@ protected override async ValueTask ProposeOrderItems(List 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) + if ((ctxGroup.Key.OpportunityType != OpportunityType.FacilityUseSlot && ctxGroup.Key.OpportunityType != OpportunityType.IndividualFacilityUseSlot) || !ctxGroup.Key.SlotId.HasValue) { throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity ID and type are as not expected for the FacilityStore, during proposal"); } diff --git a/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json b/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json index c1210ca3..bfd50f99 100644 --- a/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json +++ b/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json @@ -1,6 +1,7 @@ { "FeatureFlags": { "SingleSeller": true, - "EnableTokenAuth": false + "EnableTokenAuth": false, + "GenerateIndividualFacilityUses": true } } diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs index caa10fbb..c5bf8926 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs @@ -111,7 +111,17 @@ protected override async Task>> GetRpdeItems(long? af PrefLabel = "Squash Court", InScheme = new Uri("https://openactive.io/facility-types") } - } + }, + IndividualFacilityUse = result.Item1.IndividualFacilityUses != null ? result.Item1.IndividualFacilityUses.Select(ifu => new OpenActive.NET.IndividualFacilityUse + { + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = ifu.Id, + FacilityUseId = result.Item1.Id + }), + Name = ifu.Name + }).ToList() : null, } }); @@ -145,7 +155,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => new RpdeItem { - Kind = RpdeKind.FacilityUseSlot, + Kind = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? RpdeKind.IndividualFacilityUseSlot : RpdeKind.FacilityUseSlot, Id = x.Id, Modified = x.Modified, State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, @@ -156,11 +166,19 @@ protected override async Task>> GetRpdeItems(long? afterTime // constant as power of configuration through underlying class grows (i.e. as new properties are added) Id = RenderOpportunityId(new FacilityOpportunity { - OpportunityType = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, }), - FacilityUse = RenderOpportunityId(new FacilityOpportunity + FacilityUse = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = x.IndividualFacilityUseId, + FacilityUseId = x.FacilityUseId, + }) + : RenderOpportunityId(new FacilityOpportunity { OpportunityType = OpportunityType.FacilityUse, FacilityUseId = x.FacilityUseId @@ -176,9 +194,10 @@ protected override async Task>> GetRpdeItems(long? afterTime Id = RenderOfferId(new FacilityOpportunity { OfferId = 0, - OpportunityType = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, }), Price = x.Price, PriceCurrency = "GBP", diff --git a/Examples/BookingSystem.AspNetFramework/IdComponents/FacilityOpportunity.cs b/Examples/BookingSystem.AspNetFramework/IdComponents/FacilityOpportunity.cs index ad6e4e7e..5084c467 100644 --- a/Examples/BookingSystem.AspNetFramework/IdComponents/FacilityOpportunity.cs +++ b/Examples/BookingSystem.AspNetFramework/IdComponents/FacilityOpportunity.cs @@ -17,6 +17,7 @@ public class FacilityOpportunity : IBookableIdComponents public long? FacilityUseId { get; set; } public long? SlotId { get; set; } public long? OfferId { get; set; } + public long? IndividualFacilityUseId { get; set; } public override bool Equals(object obj) { @@ -27,7 +28,8 @@ public override bool Equals(object obj) return OpportunityType == other.OpportunityType && FacilityUseId == other.FacilityUseId && SlotId == other.SlotId && - OfferId == other.OfferId; + OfferId == other.OfferId && + IndividualFacilityUseId == other.IndividualFacilityUseId; } public override int GetHashCode() @@ -39,6 +41,7 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ FacilityUseId.GetHashCode(); hashCode = (hashCode * 397) ^ SlotId.GetHashCode(); hashCode = (hashCode * 397) ^ OfferId.GetHashCode(); + hashCode = (hashCode * 397) ^ IndividualFacilityUseId.GetHashCode(); // ReSharper enable NonReadonlyMemberInGetHashCode return hashCode; } diff --git a/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs index 6ead5201..1cce79c1 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs @@ -18,6 +18,7 @@ public class FeatureSettings public bool PaymentReconciliationDetailValidation { get; set; } = true; public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; + public bool GenerateIndividualFacilityUses { get; set; } = false; } public class PaymentSettings diff --git a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs index 2f01e945..4ae4c03b 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs @@ -11,6 +11,51 @@ public static class EngineConfig { public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSettings) { + var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.IndividualFacilityUseSlot, + AssignedFeed = OpportunityType.IndividualFacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + Bookable = true + }, + // Parent + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.IndividualFacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}" + }, + // Grandparent + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.FacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + : + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.FacilityUseSlot, + AssignedFeed = OpportunityType.FacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + Bookable = true + }, + // Parent + new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.FacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + ; + return new StoreBookingEngine( new BookingEngineSettings { @@ -37,24 +82,8 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting Bookable = false }), - new BookablePairIdTemplate ( - // Opportunity - new OpportunityIdConfiguration - { - OpportunityType = OpportunityType.FacilityUseSlot, - AssignedFeed = OpportunityType.FacilityUseSlot, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}", - OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", - Bookable = true - }, - // Parent - new OpportunityIdConfiguration - { - OpportunityType = OpportunityType.FacilityUse, - AssignedFeed = OpportunityType.FacilityUse, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" - })/*, - + facilityBookablePaidIdTemplate, + /* new BookablePairIdTemplate( // Opportunity new OpportunityIdConfiguration @@ -147,7 +176,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OpportunityType.FacilityUse, new AcmeFacilityUseRpdeGenerator(appSettings) }, { - OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings) + appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings) } }, @@ -252,7 +281,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting new SessionStore(appSettings), new List { OpportunityType.ScheduledSession } }, { - new FacilityStore(appSettings), new List { OpportunityType.FacilityUseSlot } + new FacilityStore(appSettings), new List { appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot } } }, OrderStore = new AcmeOrderStore(appSettings), diff --git a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs index ef3e865c..7eba84d5 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs @@ -35,197 +35,218 @@ protected override async Task CreateOpportunityWithinTestDa long? sellerId = _appSettings.FeatureFlags.SingleSeller ? null : seller.IdLong; var requiresApproval = openBookingFlow == TestOpenBookingFlowEnumeration.OpenBookingApprovalFlow; + var generateIndividualFacilityUses = _appSettings.FeatureFlags.GenerateIndividualFacilityUses; switch (opportunityType) { case OpportunityType.FacilityUseSlot: + case OpportunityType.IndividualFacilityUseSlot: int facilityId, slotId; + int? individualFacilityUseId; switch (criteria) { case TestOpportunityCriteriaEnumeration.TestOpportunityBookable: case TestOpportunityCriteriaEnumeration.TestOpportunityOfflineBookable: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility", rnd.Next(2) == 0 ? 0M : 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellable: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFree: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableUsingPayment: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableFree: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility", 0M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithinValidFromBeforeStartDate: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableOutsideValidFromBeforeStartDate: { var isValid = criteria == TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithinValidFromBeforeStartDate; - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, $"[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility {(isValid ? "Within" : "Outside")} Window", 14.99M, 10, requiresApproval, - validFromStartDate: isValid); + validFromStartDate: isValid, + generateIndividualFacilityUses: generateIndividualFacilityUses); } break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellableWithinWindow: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellableOutsideWindow: { var isValid = criteria == TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellableWithinWindow; - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, $"[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility {(isValid ? "Within" : "Outside")} Cancellation Window", 14.99M, 10, requiresApproval, - latestCancellationBeforeStartDate: isValid); + latestCancellationBeforeStartDate: isValid, + generateIndividualFacilityUses: generateIndividualFacilityUses); } break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentOptional: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Prepayment Optional", 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Optional); + prepayment: RequiredStatusType.Optional, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentUnavailable: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Prepayment Unavailable", 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Unavailable); + prepayment: RequiredStatusType.Unavailable, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentRequired: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Prepayment Required", 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Required); + prepayment: RequiredStatusType.Required, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNoSpaces: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility No Spaces", 14.99M, 0, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableFiveSpaces: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility Five Spaces", 14.99M, 5, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableOneSpace: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility One Space", 14.99M, 1, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreeTaxNet: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 2, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Tax Net", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreeTaxGross: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 1, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Tax Gross", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableSellerTermsOfService: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 1, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility With Seller Terms Of Service", 14.99M, 10, - requiresApproval); + requiresApproval, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableAttendeeDetails: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, 1, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility That Requires Attendee Details", 14.99M, 10, requiresApproval, - requiresAttendeeValidation: true); + requiresAttendeeValidation: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableAdditionalDetails: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility That Requires Additional Details", 10M, 10, requiresApproval, - requiresAdditionalDetails: true); + requiresAdditionalDetails: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithNegotiation: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility That Allows Proposal Amendment", 10M, 10, requiresApproval, - allowProposalAmendment: true); + allowProposalAmendment: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNotCancellable: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility Paid That Does Not Allow Full Refund", 10M, 10, requiresApproval, - allowCustomerCancellationFullRefund: false); + allowCustomerCancellationFullRefund: false, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableInPast: - (facilityId, slotId) = await FakeBookingSystem.Database.AddFacility( + (facilityId, individualFacilityUseId, slotId) = await FakeBookingSystem.Database.AddFacility( testDatasetIdentifier, sellerId, "[OPEN BOOKING API TEST INTERFACE] Bookable Facility in the Past", @@ -233,7 +254,8 @@ protected override async Task CreateOpportunityWithinTestDa 10, requiresApproval, allowCustomerCancellationFullRefund: false, - inPast: true); + inPast: true, + generateIndividualFacilityUses: generateIndividualFacilityUses); break; default: throw new OpenBookingException(new OpenBookingError(), "testOpportunityCriteria value not supported"); @@ -243,7 +265,8 @@ protected override async Task CreateOpportunityWithinTestDa { OpportunityType = opportunityType, FacilityUseId = facilityId, - SlotId = slotId + SlotId = slotId, + IndividualFacilityUseId = individualFacilityUseId }; default: throw new OpenBookingException(new OpenBookingError(), "Opportunity Type not supported"); @@ -302,6 +325,79 @@ protected override async Task GetOrderItems(List ifu.Id == slot.IndividualFacilityUseId); + slotParent = new OpenActive.NET.IndividualFacilityUse + { + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = individualFacilityUse.Id, + FacilityUseId = facility.Id, + }), + Name = individualFacilityUse.Name, + AggregateFacilityUse = 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, + } + }, + FacilityType = new List { + new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + } + } + } + }; + } + else + { + slotParent = 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, + } + }, + FacilityType = new List { + new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + } + } + }; + } + return new { OrderItem = new OrderItem @@ -324,37 +420,12 @@ protected override async Task GetOrderItems(List { - new Concept - { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", - InScheme = new Uri("https://openactive.io/facility-types") - } - } - }, + FacilityUse = slotParent, StartDate = (DateTimeOffset)slot.Start, EndDate = (DateTimeOffset)slot.End, MaximumUses = slot.MaximumUses, @@ -422,7 +493,7 @@ protected override async ValueTask LeaseOrderItems(Lease lease, List !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) + if ((ctxGroup.Key.OpportunityType != OpportunityType.FacilityUseSlot && ctxGroup.Key.OpportunityType != OpportunityType.IndividualFacilityUseSlot) || !ctxGroup.Key.SlotId.HasValue) { foreach (var ctx in ctxGroup) { @@ -494,7 +565,7 @@ protected override async ValueTask BookOrderItems(List 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) + if ((ctxGroup.Key.OpportunityType != OpportunityType.FacilityUseSlot && ctxGroup.Key.OpportunityType != OpportunityType.IndividualFacilityUseSlot) || !ctxGroup.Key.SlotId.HasValue) { throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity ID and type are as not expected for the FacilityStore, during booking"); } @@ -557,7 +628,7 @@ protected override async ValueTask ProposeOrderItems(List 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) + if ((ctxGroup.Key.OpportunityType != OpportunityType.FacilityUseSlot && ctxGroup.Key.OpportunityType != OpportunityType.IndividualFacilityUseSlot) || !ctxGroup.Key.SlotId.HasValue) { throw new OpenBookingException(new UnableToProcessOrderItemError(), "Opportunity ID and type are as not expected for the FacilityStore, during proposal"); } diff --git a/Examples/BookingSystem.AspNetFramework/Web.config b/Examples/BookingSystem.AspNetFramework/Web.config index c13ebba0..767c1399 100644 --- a/Examples/BookingSystem.AspNetFramework/Web.config +++ b/Examples/BookingSystem.AspNetFramework/Web.config @@ -4,21 +4,21 @@ https://go.microsoft.com/fwlink/?LinkId=301879 --> - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index 120a2482..4d329079 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -12,6 +12,7 @@ using System.Text; using System.Threading.Tasks; using ServiceStack.OrmLite.Dapper; +using OpenActive.NET; namespace OpenActive.FakeDatabase.NET { @@ -1374,6 +1375,7 @@ public static async Task RecalculateSpaces(IDbConnection db, IEnumerable o public static async Task GetPrepopulatedFakeDatabase() { var database = new FakeDatabase(); + var generateIndividualFacilityUses = bool.TryParse(Environment.GetEnvironmentVariable("GENERATE_INDIVIDUAL_FACILITY_USES"), out var generateIfuEnvVar) ? generateIfuEnvVar : false; using (var db = await database.Mem.Database.OpenAsync()) using (var transaction = db.OpenTransaction(IsolationLevel.Serializable)) { @@ -1381,69 +1383,130 @@ public static async Task GetPrepopulatedFakeDatabase() await CreateSellers(db); await CreateSellerUsers(db); await CreateFakeClasses(db); - await CreateFakeFacilitiesAndSlots(db); + await CreateFakeFacilitiesAndSlots(db, generateIndividualFacilityUses); await CreateOrders(db); // Add these in to generate your own orders and grants, otherwise generate them using the test suite await CreateGrants(db); await BookingPartnerTable.Create(db); transaction.Commit(); + } return database; } - private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db) + private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool generateIndividualFacilityUses) { var opportunitySeeds = GenerateOpportunitySeedDistribution(OpportunityCount); - var facilities = opportunitySeeds - .Select(seed => new FacilityUseTable - { - Id = seed.Id, - Deleted = false, - Name = $"{Faker.Commerce.ProductMaterial()} {Faker.PickRandomParam("Sports Hall", "Swimming Pool Hall", "Running Hall", "Jumping Hall")}", - SellerId = Faker.Random.Bool(0.8f) ? Faker.Random.Long(1, 2) : Faker.Random.Long(3, 5), // distribution: 80% 1-2, 20% 3-5 - }) - .AsList(); - var slotId = 0; - var slots = opportunitySeeds.Select(seed => - Enumerable.Range(0, 10) - .Select(_ => new - { - StartDate = seed.RandomStartDate(), - TotalUses = Faker.Random.Int(0, 8), - Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price(0, 20)), - }) - .Select(slot => - { - var requiresAdditionalDetails = Faker.Random.Bool(ProportionWithRequiresAdditionalDetails); - return new SlotTable - { - FacilityUseId = seed.Id, - Id = slotId++, - Deleted = false, - Start = slot.StartDate, - End = slot.StartDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), - MaximumUses = slot.TotalUses, - RemainingUses = slot.TotalUses, - Price = slot.Price, - Prepayment = slot.Price == 0 - ? Faker.Random.Bool() ? RequiredStatusType.Unavailable : (RequiredStatusType?)null - : Faker.Random.Bool() ? Faker.Random.Enum() : (RequiredStatusType?)null, - RequiresAttendeeValidation = Faker.Random.Bool(ProportionWithRequiresAttendeeValidation), - RequiresAdditionalDetails = requiresAdditionalDetails, - RequiredAdditionalDetails = requiresAdditionalDetails ? PickRandomAdditionalDetails() : null, - RequiresApproval = seed.RequiresApproval, - AllowsProposalAmendment = seed.RequiresApproval && Faker.Random.Bool(), - ValidFromBeforeStartDate = seed.RandomValidFromBeforeStartDate(), - LatestCancellationBeforeStartDate = RandomLatestCancellationBeforeStartDate(), - AllowCustomerCancellationFullRefund = Faker.Random.Bool() - }; - } - )).SelectMany(os => os); - + List<(FacilityUseTable facility, List slots)> facilitiesAndSlots = opportunitySeeds.Select((seed) => + { + var facilityUseName = $"{Faker.Commerce.ProductMaterial()} {Faker.PickRandomParam("Sports Hall", "Swimming Pool Hall", "Running Hall", "Jumping Hall")}"; + // Create random FacilityUses + var facility = new FacilityUseTable + { + Id = seed.Id, + Deleted = false, + Name = facilityUseName, + SellerId = Faker.Random.Bool(0.8f) ? Faker.Random.Long(1, 2) : Faker.Random.Long(3, 5), // distribution: 80% 1-2, 20% 3-5 + }; + + // If generateIndividualFacilityUses=true, generate 10 IFUs with each with a randomly generated number of Slots each with MaximumUses=1 + if (generateIndividualFacilityUses) + { + // Create random Individual Facility Uses + var individualFacilityUses = Enumerable.Range(0, 10).Select(i => new IndividualFacilityUse + { + Id = i, + Name = $"Court {i} at {facility.Name}", + SportActivityLocationName = $"Court {i}" + }).AsList(); + facility.IndividualFacilityUses = individualFacilityUses; + + // Create random Slots + var slots = individualFacilityUses.Select(ifu => new + { + StartDate = seed.RandomStartDate(), + TotalUses = 1, + Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price(0, 20)), + IndividualFacilityUseId = ifu.Id, + }) + .Select(slot => + { + var requiresAdditionalDetails = Faker.Random.Bool(ProportionWithRequiresAdditionalDetails); + return new SlotTable + { + FacilityUseId = seed.Id, + IndividualFacilityUseId = slot.IndividualFacilityUseId, + Id = slotId++, + Deleted = false, + Start = slot.StartDate, + End = slot.StartDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), + MaximumUses = slot.TotalUses, + RemainingUses = slot.TotalUses, + Price = slot.Price, + Prepayment = slot.Price == 0 + ? Faker.Random.Bool() ? RequiredStatusType.Unavailable : (RequiredStatusType?)null + : Faker.Random.Bool() ? Faker.Random.Enum() : (RequiredStatusType?)null, + RequiresAttendeeValidation = Faker.Random.Bool(ProportionWithRequiresAttendeeValidation), + RequiresAdditionalDetails = requiresAdditionalDetails, + RequiredAdditionalDetails = requiresAdditionalDetails ? PickRandomAdditionalDetails() : null, + RequiresApproval = seed.RequiresApproval, + AllowsProposalAmendment = seed.RequiresApproval && Faker.Random.Bool(), + ValidFromBeforeStartDate = seed.RandomValidFromBeforeStartDate(), + LatestCancellationBeforeStartDate = RandomLatestCancellationBeforeStartDate(), + AllowCustomerCancellationFullRefund = Faker.Random.Bool() + }; + }).AsList(); + return (facility, slots); + } + + else + { + var slots = Enumerable.Range(0, 10) + .Select(_ => new + { + StartDate = seed.RandomStartDate(), + TotalUses = Faker.Random.Int(0, 8), + Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price(0, 20)), + }) + .Select(slot => + { + var requiresAdditionalDetails = Faker.Random.Bool(ProportionWithRequiresAdditionalDetails); + return new SlotTable + { + FacilityUseId = seed.Id, + Id = slotId++, + Deleted = false, + Start = slot.StartDate, + End = slot.StartDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), + MaximumUses = slot.TotalUses, + RemainingUses = slot.TotalUses, + Price = slot.Price, + Prepayment = slot.Price == 0 + ? Faker.Random.Bool() ? RequiredStatusType.Unavailable : (RequiredStatusType?)null + : Faker.Random.Bool() ? Faker.Random.Enum() : (RequiredStatusType?)null, + RequiresAttendeeValidation = Faker.Random.Bool(ProportionWithRequiresAttendeeValidation), + RequiresAdditionalDetails = requiresAdditionalDetails, + RequiredAdditionalDetails = requiresAdditionalDetails ? PickRandomAdditionalDetails() : null, + RequiresApproval = seed.RequiresApproval, + AllowsProposalAmendment = seed.RequiresApproval && Faker.Random.Bool(), + ValidFromBeforeStartDate = seed.RandomValidFromBeforeStartDate(), + LatestCancellationBeforeStartDate = RandomLatestCancellationBeforeStartDate(), + AllowCustomerCancellationFullRefund = Faker.Random.Bool() + }; + } + ).AsList(); + return (facility, slots); + } + + }) + .AsList(); + + var facilities = facilitiesAndSlots.Select(facilityAndSlots => facilityAndSlots.facility); + var slotTableSlots = facilitiesAndSlots.SelectMany(facilityAndSlots => facilityAndSlots.slots); await db.InsertAllAsync(facilities); - await db.InsertAllAsync(slots); + await db.InsertAllAsync(slotTableSlots); } public static async Task CreateFakeClasses(IDbConnection db) @@ -1822,7 +1885,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli } } - public async Task<(int, int)> AddFacility( + public async Task<(int, int?, int)> AddFacility( string testDatasetIdentifier, long? sellerId, string title, @@ -1838,7 +1901,9 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli decimal locationLat = 0.1m, decimal locationLng = 0.1m, bool allowProposalAmendment = false, - bool inPast = false) + bool inPast = false, + bool generateIndividualFacilityUses = false + ) { var startTime = DateTime.Now.AddDays(inPast ? -1 : 1); var endTime = DateTime.Now.AddDays(inPast ? -1 : 1).AddHours(1); @@ -1846,6 +1911,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli using (var db = await Mem.Database.OpenAsync()) using (var transaction = db.OpenTransaction(IsolationLevel.Serializable)) { + var facility = new FacilityUseTable { TestDatasetIdentifier = testDatasetIdentifier, @@ -1854,10 +1920,21 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli SellerId = sellerId ?? 1, LocationLat = locationLat, LocationLng = locationLng, - Modified = DateTimeOffset.Now.UtcTicks + Modified = DateTimeOffset.Now.UtcTicks, }; + if (generateIndividualFacilityUses) + { + facility.IndividualFacilityUses = new List { + new IndividualFacilityUse { + Id = 1, + Name = $"Court {1} on {title}", + SportActivityLocationName = $"Court {1}" + } + }; + } await db.SaveAsync(facility); + int? individualFacilityUseId = null; var slot = new SlotTable { TestDatasetIdentifier = testDatasetIdentifier, @@ -1883,11 +1960,17 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli AllowCustomerCancellationFullRefund = allowCustomerCancellationFullRefund, Modified = DateTimeOffset.Now.UtcTicks }; + if (generateIndividualFacilityUses) + { + individualFacilityUseId = 1; + slot.IndividualFacilityUseId = individualFacilityUseId; + } await db.SaveAsync(slot); transaction.Commit(); - return ((int)facility.Id, (int)slot.Id); + + return ((int)facility.Id, individualFacilityUseId, (int)slot.Id); } } diff --git a/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs b/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs index c875db32..066ac1b9 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs @@ -1,7 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; using ServiceStack.DataAnnotations; namespace OpenActive.FakeDatabase.NET { + public class IndividualFacilityUse + { + public long Id { get; set; } + public string Name { get; set; } + public string SportActivityLocationName { get; set; } + + } + public class FacilityUseTable : Table { public string TestDatasetIdentifier { get; set; } @@ -13,5 +23,19 @@ public class FacilityUseTable : Table public long SellerId { get; set; } // Provider public decimal LocationLat { get; set; } public decimal LocationLng { get; set; } + //private string IndividualFacilityUseString { get; set; } + //[Ignore] + //public List IndividualFacilityUses + //{ + // get + // { + // return JsonConvert.DeserializeObject>(IndividualFacilityUseString); + // } + // set + // { + // IndividualFacilityUseString = JsonConvert.SerializeObject(IndividualFacilityUses); + // } + //} + public List IndividualFacilityUses { get; set; } } } \ No newline at end of file diff --git a/Fakes/OpenActive.FakeDatabase.NET/Models/SlotTable.cs b/Fakes/OpenActive.FakeDatabase.NET/Models/SlotTable.cs index 35fea58e..895179a1 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/Models/SlotTable.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/Models/SlotTable.cs @@ -11,6 +11,7 @@ public class SlotTable : Table public FacilityUseTable FacilityUseTable { get; set; } [ForeignKey(typeof(FacilityUseTable), OnDelete = "CASCADE")] public long FacilityUseId { get; set; } + public long? IndividualFacilityUseId { get; set; } public DateTime Start { get; set; } public DateTime End { get; set; } public long MaximumUses { get; set; } @@ -26,5 +27,6 @@ public class SlotTable : Table public TimeSpan? ValidFromBeforeStartDate { get; set; } public TimeSpan? LatestCancellationBeforeStartDate { get; set; } public bool AllowsProposalAmendment { get; set; } + } } \ No newline at end of file diff --git a/OpenActive.Server.NET/OpenBookingHelper/IdTransforms/IdTemplate.cs b/OpenActive.Server.NET/OpenBookingHelper/IdTransforms/IdTemplate.cs index 8ef8bdf5..61425cce 100644 --- a/OpenActive.Server.NET/OpenBookingHelper/IdTransforms/IdTemplate.cs +++ b/OpenActive.Server.NET/OpenBookingHelper/IdTransforms/IdTemplate.cs @@ -371,7 +371,7 @@ public Uri RenderOfferId(OpportunityType opportunityType, TBookableIdComponents else if (opportunityType == ParentIdConfiguration?.OpportunityType) return RenderId(3, components, nameof(RenderOfferId), "parentOfferUriTemplate"); else if (opportunityType == GrandparentIdConfiguration?.OpportunityType) - return RenderId(5, components, nameof(RenderOfferId), "parentOfferUriTemplate"); + return RenderId(5, components, nameof(RenderOfferId), "grandparentOfferUriTemplate"); else throw new ArgumentOutOfRangeException(nameof(opportunityType), "OpportunityType was not found within this template"); } From 7016abc71b9bd5b584f9d3ac250f69eb04514b8a Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 14 Jun 2023 10:17:27 +0100 Subject: [PATCH 02/14] revert appsettings changes --- .../Properties/launchSettings.json | 9 ++++++--- .../appsettings.single-seller.json | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json index 5828de08..30b0586b 100644 --- a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json +++ b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json @@ -1,4 +1,8 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true + }, "profiles": { "BookingSystem.AspNetCore": { "commandName": "Project", @@ -6,9 +10,8 @@ "launchUrl": "https://localhost:5001/openactive", "applicationUrl": "https://localhost:5001", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "single-seller", - "GENERATE_INDIVIDUAL_FACILITY_USES": "true", - "OPPORTUNITY_COUNT": "200" + "ASPNETCORE_ENVIRONMENT": "Development", + "ApplicationHostBaseUrl": "https://localhost:5001" } } } diff --git a/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json b/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json index bfd50f99..c1210ca3 100644 --- a/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json +++ b/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json @@ -1,7 +1,6 @@ { "FeatureFlags": { "SingleSeller": true, - "EnableTokenAuth": false, - "GenerateIndividualFacilityUses": true + "EnableTokenAuth": false } } From c04cddd34bef26f750ec8660cc94d2aab5e704fb Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 15 Jun 2023 16:13:26 +0100 Subject: [PATCH 03/14] FakeDatabase configured by appsettings --- .../Custom/Settings/AppSettings.cs | 1 + .../Startup.cs | 2 +- Examples/BookingSystem.AspNetCore/Startup.cs | 2 +- .../Stores/FacilityStore.cs | 58 ++++++------------- .../FakeBookingSystem.cs | 23 ++++---- 5 files changed, 35 insertions(+), 51 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs index afc12089..80849901 100644 --- a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs @@ -3,5 +3,6 @@ public class AppSettings { public string JsonLdIdBaseUrl { get; set; } + public bool GenerateIndividualFacilityUses { get; set; } } } \ No newline at end of file diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs b/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs index 45539b1e..dab26a07 100644 --- a/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs @@ -27,7 +27,7 @@ public Startup(IWebHostEnvironment environment, IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(); + services.AddSingleton(x => new FakeBookingSystem(AppSettings.GenerateIndividualFacilityUses)); var builder = services.AddIdentityServer(options => { diff --git a/Examples/BookingSystem.AspNetCore/Startup.cs b/Examples/BookingSystem.AspNetCore/Startup.cs index 5b995fc2..b2beb087 100644 --- a/Examples/BookingSystem.AspNetCore/Startup.cs +++ b/Examples/BookingSystem.AspNetCore/Startup.cs @@ -74,7 +74,7 @@ public void ConfigureServices(IServiceCollection services) .AddControllers() .AddMvcOptions(options => options.InputFormatters.Insert(0, new OpenBookingInputFormatter())); - services.AddSingleton(sp => EngineConfig.CreateStoreBookingEngine(AppSettings, new FakeBookingSystem())); + services.AddSingleton(sp => EngineConfig.CreateStoreBookingEngine(AppSettings, new FakeBookingSystem(AppSettings.FeatureFlags.GenerateIndividualFacilityUses))); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs index ab926be0..612d6952 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs @@ -37,7 +37,6 @@ protected override async Task CreateOpportunityWithinTestDa long? sellerId = _appSettings.FeatureFlags.SingleSeller ? null : seller.IdLong; var requiresApproval = openBookingFlow == TestOpenBookingFlowEnumeration.OpenBookingApprovalFlow; - var generateIndividualFacilityUses = _appSettings.FeatureFlags.GenerateIndividualFacilityUses; switch (opportunityType) { @@ -55,8 +54,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Facility", rnd.Next(2) == 0 ? 0M : 14.99M, 10, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellable: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFree: @@ -67,8 +65,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility", 14.99M, 10, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableFree: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -77,8 +74,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility", 0M, 10, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithinValidFromBeforeStartDate: case TestOpportunityCriteriaEnumeration.TestOpportunityBookableOutsideValidFromBeforeStartDate: @@ -92,8 +88,7 @@ protected override async Task CreateOpportunityWithinTestDa 14.99M, 10, requiresApproval, - validFromStartDate: isValid, - generateIndividualFacilityUses: generateIndividualFacilityUses); + validFromStartDate: isValid); } break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableCancellableWithinWindow: @@ -107,8 +102,7 @@ protected override async Task CreateOpportunityWithinTestDa 14.99M, 10, requiresApproval, - latestCancellationBeforeStartDate: isValid, - generateIndividualFacilityUses: generateIndividualFacilityUses); + latestCancellationBeforeStartDate: isValid); } break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentOptional: @@ -119,8 +113,7 @@ protected override async Task CreateOpportunityWithinTestDa 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Optional, - generateIndividualFacilityUses: generateIndividualFacilityUses); + prepayment: RequiredStatusType.Optional); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentUnavailable: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -130,8 +123,7 @@ protected override async Task CreateOpportunityWithinTestDa 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Unavailable, - generateIndividualFacilityUses: generateIndividualFacilityUses); + prepayment: RequiredStatusType.Unavailable); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreePrepaymentRequired: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -141,8 +133,7 @@ protected override async Task CreateOpportunityWithinTestDa 14.99M, 10, requiresApproval, - prepayment: RequiredStatusType.Required, - generateIndividualFacilityUses: generateIndividualFacilityUses); + prepayment: RequiredStatusType.Required); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNoSpaces: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -151,8 +142,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility No Spaces", 14.99M, 0, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableFiveSpaces: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -161,8 +151,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility Five Spaces", 14.99M, 5, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableOneSpace: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -171,8 +160,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Free Facility One Space", 14.99M, 1, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreeTaxNet: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -181,8 +169,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Tax Net", 14.99M, 10, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNonFreeTaxGross: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -191,8 +178,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Paid Facility Tax Gross", 14.99M, 10, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableSellerTermsOfService: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -201,8 +187,7 @@ protected override async Task CreateOpportunityWithinTestDa "[OPEN BOOKING API TEST INTERFACE] Bookable Facility With Seller Terms Of Service", 14.99M, 10, - requiresApproval, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresApproval); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableAttendeeDetails: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -212,8 +197,7 @@ protected override async Task CreateOpportunityWithinTestDa 14.99M, 10, requiresApproval, - requiresAttendeeValidation: true, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresAttendeeValidation: true); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableAdditionalDetails: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -223,8 +207,7 @@ protected override async Task CreateOpportunityWithinTestDa 10M, 10, requiresApproval, - requiresAdditionalDetails: true, - generateIndividualFacilityUses: generateIndividualFacilityUses); + requiresAdditionalDetails: true); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableWithNegotiation: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -234,8 +217,7 @@ protected override async Task CreateOpportunityWithinTestDa 10M, 10, requiresApproval, - allowProposalAmendment: true, - generateIndividualFacilityUses: generateIndividualFacilityUses); + allowProposalAmendment: true); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableNotCancellable: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -245,8 +227,7 @@ protected override async Task CreateOpportunityWithinTestDa 10M, 10, requiresApproval, - allowCustomerCancellationFullRefund: false, - generateIndividualFacilityUses: generateIndividualFacilityUses); + allowCustomerCancellationFullRefund: false); break; case TestOpportunityCriteriaEnumeration.TestOpportunityBookableInPast: (facilityId, individualFacilityUseId, slotId) = await _fakeBookingSystem.Database.AddFacility( @@ -257,8 +238,7 @@ protected override async Task CreateOpportunityWithinTestDa 10, requiresApproval, allowCustomerCancellationFullRefund: false, - inPast: true, - generateIndividualFacilityUses: generateIndividualFacilityUses); + inPast: true); break; default: throw new OpenBookingException(new OpenBookingError(), "testOpportunityCriteria value not supported"); diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index 6c6c4278..d755845c 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -23,9 +23,9 @@ namespace OpenActive.FakeDatabase.NET public class FakeBookingSystem { public FakeDatabase Database { get; set; } - public FakeBookingSystem() + public FakeBookingSystem(bool generateIndividualFacilityUses) { - Database = FakeDatabase.GetPrepopulatedFakeDatabase().Result; + Database = FakeDatabase.GetPrepopulatedFakeDatabase(generateIndividualFacilityUses).Result; } } @@ -146,11 +146,16 @@ public class FakeDatabase { private const float ProportionWithRequiresAttendeeValidation = 1f / 10; private const float ProportionWithRequiresAdditionalDetails = 1f / 10; - + private bool _generateIndividualFacilityUses; public readonly InMemorySQLite Mem = new InMemorySQLite(); private static readonly Faker Faker = new Faker(); + public FakeDatabase(bool generateIndividualFacilityUses) + { + _generateIndividualFacilityUses = generateIndividualFacilityUses; + } + static FakeDatabase() { Randomizer.Seed = new Random((int)(DateTime.Today - new DateTime(1970, 1, 1)).TotalDays); @@ -1352,10 +1357,9 @@ public static async Task RecalculateSpaces(IDbConnection db, IEnumerable o } } - public static async Task GetPrepopulatedFakeDatabase() + public static async Task GetPrepopulatedFakeDatabase(bool generateIndividualFacilityUses) { - var database = new FakeDatabase(); - var generateIndividualFacilityUses = bool.TryParse(Environment.GetEnvironmentVariable("GENERATE_INDIVIDUAL_FACILITY_USES"), out var generateIfuEnvVar) ? generateIfuEnvVar : false; + var database = new FakeDatabase(generateIndividualFacilityUses); using (var db = await database.Mem.Database.OpenAsync()) using (var transaction = db.OpenTransaction(IsolationLevel.Serializable)) { @@ -1881,8 +1885,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli decimal locationLat = 0.1m, decimal locationLng = 0.1m, bool allowProposalAmendment = false, - bool inPast = false, - bool generateIndividualFacilityUses = false + bool inPast = false ) { var startTime = DateTime.Now.AddDays(inPast ? -1 : 1); @@ -1902,7 +1905,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli LocationLng = locationLng, Modified = DateTimeOffset.Now.UtcTicks, }; - if (generateIndividualFacilityUses) + if (_generateIndividualFacilityUses) { facility.IndividualFacilityUses = new List { new IndividualFacilityUse { @@ -1940,7 +1943,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli AllowCustomerCancellationFullRefund = allowCustomerCancellationFullRefund, Modified = DateTimeOffset.Now.UtcTicks }; - if (generateIndividualFacilityUses) + if (_generateIndividualFacilityUses) { individualFacilityUseId = 1; slot.IndividualFacilityUseId = individualFacilityUseId; From 1d775816fec53c65ad5d352e3c007be9782858f5 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Mon, 19 Jun 2023 16:03:48 +0100 Subject: [PATCH 04/14] feat: add minimum price for randomly generated priced opportunities --- Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index d755845c..fd8620aa 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -1412,7 +1412,7 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool ge { StartDate = seed.RandomStartDate(), TotalUses = 1, - Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price(0, 20)), + Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), IndividualFacilityUseId = ifu.Id, }) .Select(slot => @@ -1452,7 +1452,7 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool ge { StartDate = seed.RandomStartDate(), TotalUses = Faker.Random.Int(0, 8), - Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price(0, 20)), + Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), }) .Select(slot => { @@ -1501,7 +1501,7 @@ public static async Task CreateFakeClasses(IDbConnection db) .Select(seed => new { seed.Id, - Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price(0, 20)), + Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), ValidFromBeforeStartDate = seed.RandomValidFromBeforeStartDate(), seed.RequiresApproval }) From c06928d11d6544fbf7fefd6ebd4183faf2e1d0f8 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 12:56:10 +0100 Subject: [PATCH 05/14] Fixes based on PR comments --- .../Custom/Settings/AppSettings.cs | 10 +- .../Startup.cs | 2 +- .../Feeds/FacilitiesFeeds.cs | 22 +- .../Settings/AppSettings.cs | 2 +- .../Settings/EngineConfig.cs | 36 ++-- Examples/BookingSystem.AspNetCore/Startup.cs | 2 +- .../Stores/FacilityStore.cs | 64 +++--- .../Feeds/FacilitiesFeeds.cs | 22 +- .../Settings/AppSettings.cs | 2 +- .../Settings/EngineConfig.cs | 36 ++-- .../Stores/FacilityStore.cs | 66 +++--- .../FakeBookingSystem.cs | 201 ++++++++---------- 12 files changed, 207 insertions(+), 258 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs index 80849901..30b29afa 100644 --- a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs @@ -3,6 +3,14 @@ public class AppSettings { public string JsonLdIdBaseUrl { get; set; } - public bool GenerateIndividualFacilityUses { get; set; } + public FeatureSettings FeatureFlags { get; set; } + + /** + * Note feature defaults are set here, and are used for the .NET Framework reference implementation + */ + public class FeatureSettings + { + public bool FacilityUseHasSlots { get; set; } = false; + } } } \ No newline at end of file diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs b/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs index dab26a07..f743c4c4 100644 --- a/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs @@ -27,7 +27,7 @@ public Startup(IWebHostEnvironment environment, IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(x => new FakeBookingSystem(AppSettings.GenerateIndividualFacilityUses)); + services.AddSingleton(x => new FakeBookingSystem(AppSettings.FeatureFlags.FacilityUseHasSlots)); var builder = services.AddIdentityServer(options => { diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 5d5154c7..30e77df2 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -143,7 +143,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => new RpdeItem { - Kind = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? RpdeKind.IndividualFacilityUseSlot : RpdeKind.FacilityUseSlot, + Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, Id = x.Id, Modified = x.Modified, State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, @@ -154,22 +154,22 @@ protected override async Task>> GetRpdeItems(long? afterTime // constant as power of configuration through underlying class grows (i.e. as new properties are added) Id = RenderOpportunityId(new FacilityOpportunity { - OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, SlotId = x.Id, - IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, }), - FacilityUse = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + FacilityUse = _appSettings.FeatureFlags.FacilityUseHasSlots ? RenderOpportunityId(new FacilityOpportunity { - OpportunityType = OpportunityType.IndividualFacilityUse, - IndividualFacilityUseId = x.IndividualFacilityUseId, - FacilityUseId = x.FacilityUseId, + OpportunityType = OpportunityType.FacilityUse, + FacilityUseId = x.FacilityUseId }) : RenderOpportunityId(new FacilityOpportunity { - OpportunityType = OpportunityType.FacilityUse, - FacilityUseId = x.FacilityUseId + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = x.IndividualFacilityUseId, + FacilityUseId = x.FacilityUseId, }), Identifier = x.Id, StartDate = (DateTimeOffset)x.Start, @@ -182,10 +182,10 @@ protected override async Task>> GetRpdeItems(long? afterTime Id = RenderOfferId(new FacilityOpportunity { OfferId = 0, - OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, SlotId = x.Id, - IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, }), Price = x.Price, PriceCurrency = "GBP", diff --git a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs index a740bffb..aa120720 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs @@ -19,7 +19,7 @@ public class FeatureSettings public bool PaymentReconciliationDetailValidation { get; set; } = true; public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; - public bool GenerateIndividualFacilityUses { get; set; } = false; + public bool FacilityUseHasSlots { get; set; } = false; } public class PaymentSettings diff --git a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs index 31b807ac..5dafa9fa 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs @@ -12,26 +12,19 @@ public static class EngineConfig { public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSettings, FakeBookingSystem fakeBookingSystem) { - var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.FacilityUseHasSlots ? new BookablePairIdTemplate( // Opportunity new OpportunityIdConfiguration { - OpportunityType = OpportunityType.IndividualFacilityUseSlot, - AssignedFeed = OpportunityType.IndividualFacilityUseSlot, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}", - OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + OpportunityType = OpportunityType.FacilityUseSlot, + AssignedFeed = OpportunityType.FacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/slots/{SlotId}#/offers/{OfferId}", Bookable = true }, // Parent new OpportunityIdConfiguration - { - OpportunityType = OpportunityType.IndividualFacilityUse, - AssignedFeed = OpportunityType.FacilityUse, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}" - }, - // Grandparent - new OpportunityIdConfiguration { OpportunityType = OpportunityType.FacilityUse, AssignedFeed = OpportunityType.FacilityUse, @@ -42,14 +35,21 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting // Opportunity new OpportunityIdConfiguration { - OpportunityType = OpportunityType.FacilityUseSlot, - AssignedFeed = OpportunityType.FacilityUseSlot, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}", - OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + OpportunityType = OpportunityType.IndividualFacilityUseSlot, + AssignedFeed = OpportunityType.IndividualFacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/slots/{SlotId}#/offers/{OfferId}", Bookable = true }, // Parent new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.IndividualFacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}" + }, + // Grandparent + new OpportunityIdConfiguration { OpportunityType = OpportunityType.FacilityUse, AssignedFeed = OpportunityType.FacilityUse, @@ -177,7 +177,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OpportunityType.FacilityUse, new AcmeFacilityUseRpdeGenerator(appSettings, fakeBookingSystem) }, { - appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) + appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) } }, @@ -282,7 +282,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting new SessionStore(appSettings, fakeBookingSystem), new List { OpportunityType.ScheduledSession } }, { - new FacilityStore(appSettings, fakeBookingSystem), new List { appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot } + new FacilityStore(appSettings, fakeBookingSystem), new List { appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot } } }, OrderStore = new AcmeOrderStore(appSettings, fakeBookingSystem), diff --git a/Examples/BookingSystem.AspNetCore/Startup.cs b/Examples/BookingSystem.AspNetCore/Startup.cs index b2beb087..4a26db6b 100644 --- a/Examples/BookingSystem.AspNetCore/Startup.cs +++ b/Examples/BookingSystem.AspNetCore/Startup.cs @@ -74,7 +74,7 @@ public void ConfigureServices(IServiceCollection services) .AddControllers() .AddMvcOptions(options => options.InputFormatters.Insert(0, new OpenBookingInputFormatter())); - services.AddSingleton(sp => EngineConfig.CreateStoreBookingEngine(AppSettings, new FakeBookingSystem(AppSettings.FeatureFlags.GenerateIndividualFacilityUses))); + services.AddSingleton(sp => EngineConfig.CreateStoreBookingEngine(AppSettings, new FakeBookingSystem(AppSettings.FeatureFlags.FacilityUseHasSlots))); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs index 55f43d0e..b466632c 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs @@ -308,6 +308,26 @@ protected override async Task GetOrderItems(List { + new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + } + } + }; + FacilityUse slotParent; if (slot.IndividualFacilityUseId != null) { @@ -321,48 +341,12 @@ protected override async Task GetOrderItems(List { - new Concept - { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", - InScheme = new Uri("https://openactive.io/facility-types") - } - } - } + AggregateFacilityUse = facilityUse, }; } else { - slotParent = 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 = _fakeBookingSystem.Database.GetPlaceById(facility.PlaceId), - FacilityType = new List { - new Concept - { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", - InScheme = new Uri("https://openactive.io/facility-types") - } - } - }; + slotParent = facilityUse; } return new @@ -387,10 +371,10 @@ protected override async Task GetOrderItems(List>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => new RpdeItem { - Kind = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? RpdeKind.IndividualFacilityUseSlot : RpdeKind.FacilityUseSlot, + Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, Id = x.Id, Modified = x.Modified, State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, @@ -154,22 +154,22 @@ protected override async Task>> GetRpdeItems(long? afterTime // constant as power of configuration through underlying class grows (i.e. as new properties are added) Id = RenderOpportunityId(new FacilityOpportunity { - OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, SlotId = x.Id, - IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, }), - FacilityUse = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + FacilityUse = _appSettings.FeatureFlags.FacilityUseHasSlots ? RenderOpportunityId(new FacilityOpportunity { - OpportunityType = OpportunityType.IndividualFacilityUse, - IndividualFacilityUseId = x.IndividualFacilityUseId, - FacilityUseId = x.FacilityUseId, + OpportunityType = OpportunityType.FacilityUse, + FacilityUseId = x.FacilityUseId }) : RenderOpportunityId(new FacilityOpportunity { - OpportunityType = OpportunityType.FacilityUse, - FacilityUseId = x.FacilityUseId + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = x.IndividualFacilityUseId, + FacilityUseId = x.FacilityUseId, }), Identifier = x.Id, StartDate = (DateTimeOffset)x.Start, @@ -182,10 +182,10 @@ protected override async Task>> GetRpdeItems(long? afterTime Id = RenderOfferId(new FacilityOpportunity { OfferId = 0, - OpportunityType = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, SlotId = x.Id, - IndividualFacilityUseId = _appSettings.FeatureFlags.GenerateIndividualFacilityUses ? x.IndividualFacilityUseId : null, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, }), Price = x.Price, PriceCurrency = "GBP", diff --git a/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs index a740bffb..aa120720 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs @@ -19,7 +19,7 @@ public class FeatureSettings public bool PaymentReconciliationDetailValidation { get; set; } = true; public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; - public bool GenerateIndividualFacilityUses { get; set; } = false; + public bool FacilityUseHasSlots { get; set; } = false; } public class PaymentSettings diff --git a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs index 31b807ac..5dafa9fa 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs @@ -12,26 +12,19 @@ public static class EngineConfig { public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSettings, FakeBookingSystem fakeBookingSystem) { - var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.GenerateIndividualFacilityUses ? + var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.FacilityUseHasSlots ? new BookablePairIdTemplate( // Opportunity new OpportunityIdConfiguration { - OpportunityType = OpportunityType.IndividualFacilityUseSlot, - AssignedFeed = OpportunityType.IndividualFacilityUseSlot, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}", - OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + OpportunityType = OpportunityType.FacilityUseSlot, + AssignedFeed = OpportunityType.FacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/slots/{SlotId}#/offers/{OfferId}", Bookable = true }, // Parent new OpportunityIdConfiguration - { - OpportunityType = OpportunityType.IndividualFacilityUse, - AssignedFeed = OpportunityType.FacilityUse, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}" - }, - // Grandparent - new OpportunityIdConfiguration { OpportunityType = OpportunityType.FacilityUse, AssignedFeed = OpportunityType.FacilityUse, @@ -42,14 +35,21 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting // Opportunity new OpportunityIdConfiguration { - OpportunityType = OpportunityType.FacilityUseSlot, - AssignedFeed = OpportunityType.FacilityUseSlot, - OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}", - OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/facility-use-slots/{SlotId}#/offers/{OfferId}", + OpportunityType = OpportunityType.IndividualFacilityUseSlot, + AssignedFeed = OpportunityType.IndividualFacilityUseSlot, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/slots/{SlotId}", + OfferIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}/slots/{SlotId}#/offers/{OfferId}", Bookable = true }, // Parent new OpportunityIdConfiguration + { + OpportunityType = OpportunityType.IndividualFacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}/individual-facility-uses/{IndividualFacilityUseId}" + }, + // Grandparent + new OpportunityIdConfiguration { OpportunityType = OpportunityType.FacilityUse, AssignedFeed = OpportunityType.FacilityUse, @@ -177,7 +177,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OpportunityType.FacilityUse, new AcmeFacilityUseRpdeGenerator(appSettings, fakeBookingSystem) }, { - appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) + appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) } }, @@ -282,7 +282,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting new SessionStore(appSettings, fakeBookingSystem), new List { OpportunityType.ScheduledSession } }, { - new FacilityStore(appSettings, fakeBookingSystem), new List { appSettings.FeatureFlags.GenerateIndividualFacilityUses ? OpportunityType.IndividualFacilityUseSlot : OpportunityType.FacilityUseSlot } + new FacilityStore(appSettings, fakeBookingSystem), new List { appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot } } }, OrderStore = new AcmeOrderStore(appSettings, fakeBookingSystem), diff --git a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs index dffadfe9..b466632c 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -308,6 +308,26 @@ protected override async Task GetOrderItems(List { + new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + } + } + }; + FacilityUse slotParent; if (slot.IndividualFacilityUseId != null) { @@ -321,48 +341,12 @@ protected override async Task GetOrderItems(List { - new Concept - { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", - InScheme = new Uri("https://openactive.io/facility-types") - } - } - } + AggregateFacilityUse = facilityUse, }; } else { - slotParent = 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 = _fakeBookingSystem.Database.GetPlaceById(facility.PlaceId), - FacilityType = new List { - new Concept - { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", - InScheme = new Uri("https://openactive.io/facility-types") - } - } - }; + slotParent = facilityUse; } return new @@ -387,10 +371,10 @@ protected override async Task GetOrderItems(List o } } - public static async Task GetPrepopulatedFakeDatabase(bool generateIndividualFacilityUses) + public static async Task GetPrepopulatedFakeDatabase(bool facilityUseHasSlots) { - var database = new FakeDatabase(generateIndividualFacilityUses); + var database = new FakeDatabase(facilityUseHasSlots); using (var db = await database.Mem.Database.OpenAsync()) using (var transaction = db.OpenTransaction(IsolationLevel.Serializable)) { @@ -1479,7 +1479,7 @@ public static async Task GetPrepopulatedFakeDatabase(bool generate await CreateSellers(db); await CreateSellerUsers(db); await CreateFakeClasses(db); - await CreateFakeFacilitiesAndSlots(db, generateIndividualFacilityUses); + await CreateFakeFacilitiesAndSlots(db, facilityUseHasSlots); await CreateOrders(db); // Add these in to generate your own orders and grants, otherwise generate them using the test suite await CreateGrants(db); await BookingPartnerTable.Create(db); @@ -1490,121 +1490,94 @@ public static async Task GetPrepopulatedFakeDatabase(bool generate return database; } - private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool generateIndividualFacilityUses) + private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool facilityUseHasSlots) { var opportunitySeeds = GenerateOpportunitySeedDistribution(OpportunityCount); var slotId = 0; List<(FacilityUseTable facility, List slots)> facilitiesAndSlots = opportunitySeeds.Select((seed) => - { - var facilityUseName = $"{Faker.Commerce.ProductMaterial()} {Faker.PickRandomParam("Sports Hall", "Swimming Pool Hall", "Running Hall", "Jumping Hall")}"; - // Create random FacilityUses - var facility = new FacilityUseTable - { - Id = seed.Id, - Deleted = false, - Name = facilityUseName, - SellerId = Faker.Random.Bool(0.8f) ? Faker.Random.Long(1, 2) : Faker.Random.Long(3, 5), // distribution: 80% 1-2, 20% 3-5 - PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }) - }; - - // If generateIndividualFacilityUses=true, generate 10 IFUs with each with a randomly generated number of Slots each with MaximumUses=1 - if (generateIndividualFacilityUses) - { - // Create random Individual Facility Uses - var individualFacilityUses = Enumerable.Range(0, 10).Select(i => new IndividualFacilityUse - { - Id = i, - Name = $"Court {i} at {facility.Name}", - SportActivityLocationName = $"Court {i}" - }).AsList(); - facility.IndividualFacilityUses = individualFacilityUses; - - // Create random Slots - var slots = individualFacilityUses.Select(ifu => new - { - StartDate = seed.RandomStartDate(), - TotalUses = 1, - Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), - IndividualFacilityUseId = ifu.Id, - }) - .Select(slot => - { - var requiresAdditionalDetails = Faker.Random.Bool(ProportionWithRequiresAdditionalDetails); - return new SlotTable - { - FacilityUseId = seed.Id, - IndividualFacilityUseId = slot.IndividualFacilityUseId, - Id = slotId++, - Deleted = false, - Start = slot.StartDate, - End = slot.StartDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), - MaximumUses = slot.TotalUses, - RemainingUses = slot.TotalUses, - Price = slot.Price, - Prepayment = slot.Price == 0 - ? Faker.Random.Bool() ? RequiredStatusType.Unavailable : (RequiredStatusType?)null - : Faker.Random.Bool() ? Faker.Random.Enum() : (RequiredStatusType?)null, - RequiresAttendeeValidation = Faker.Random.Bool(ProportionWithRequiresAttendeeValidation), - RequiresAdditionalDetails = requiresAdditionalDetails, - RequiredAdditionalDetails = requiresAdditionalDetails ? PickRandomAdditionalDetails() : null, - RequiresApproval = seed.RequiresApproval, - AllowsProposalAmendment = seed.RequiresApproval && Faker.Random.Bool(), - ValidFromBeforeStartDate = seed.RandomValidFromBeforeStartDate(), - LatestCancellationBeforeStartDate = RandomLatestCancellationBeforeStartDate(), - AllowCustomerCancellationFullRefund = Faker.Random.Bool() - }; - }).AsList(); - return (facility, slots); - } - - else - { - var slots = Enumerable.Range(0, 10) - .Select(_ => new - { - StartDate = seed.RandomStartDate(), - TotalUses = Faker.Random.Int(0, 8), - Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), - }) - .Select(slot => - { - var requiresAdditionalDetails = Faker.Random.Bool(ProportionWithRequiresAdditionalDetails); - return new SlotTable - { - FacilityUseId = seed.Id, - Id = slotId++, - Deleted = false, - Start = slot.StartDate, - End = slot.StartDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), - MaximumUses = slot.TotalUses, - RemainingUses = slot.TotalUses, - Price = slot.Price, - Prepayment = slot.Price == 0 - ? Faker.Random.Bool() ? RequiredStatusType.Unavailable : (RequiredStatusType?)null - : Faker.Random.Bool() ? Faker.Random.Enum() : (RequiredStatusType?)null, - RequiresAttendeeValidation = Faker.Random.Bool(ProportionWithRequiresAttendeeValidation), - RequiresAdditionalDetails = requiresAdditionalDetails, - RequiredAdditionalDetails = requiresAdditionalDetails ? PickRandomAdditionalDetails() : null, - RequiresApproval = seed.RequiresApproval, - AllowsProposalAmendment = seed.RequiresApproval && Faker.Random.Bool(), - ValidFromBeforeStartDate = seed.RandomValidFromBeforeStartDate(), - LatestCancellationBeforeStartDate = RandomLatestCancellationBeforeStartDate(), - AllowCustomerCancellationFullRefund = Faker.Random.Bool() - }; - } - ).AsList(); - return (facility, slots); - } - - }) - .AsList(); + { + var facilityUseName = $"{Faker.Commerce.ProductMaterial()} {Faker.PickRandomParam("Sports Hall", "Swimming Pool Hall", "Running Hall", "Jumping Hall")}"; + var facility = new FacilityUseTable + { + Id = seed.Id, + Deleted = false, + Name = facilityUseName, + SellerId = Faker.Random.Bool(0.8f) ? Faker.Random.Long(1, 2) : Faker.Random.Long(3, 5), // distribution: 80% 1-2, 20% 3-5 + PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }) + }; + + // If facilityUseHasSlots=false, generate 10 IFUs with each with a randomly generated number of Slots each with MaximumUses=1 + List slots; + if (!facilityUseHasSlots) + { + // Create random Individual Facility Uses + var individualFacilityUses = Enumerable.Range(0, 10).Select(i => new IndividualFacilityUse + { + Id = i, + Name = $"Court {i} at {facility.Name}", + SportActivityLocationName = $"Court {i}" + }).AsList(); + facility.IndividualFacilityUses = individualFacilityUses; + + slots = individualFacilityUses.Select(ifu => new + { + StartDate = seed.RandomStartDate(), + TotalUses = 1, + Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), + IndividualFacilityUseId = ifu.Id, + }) + .Select(slot => GenerateSlot(seed, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) + .AsList(); + } + else + { + slots = Enumerable.Range(0, 10) + .Select(_ => new + { + StartDate = seed.RandomStartDate(), + TotalUses = Faker.Random.Int(0, 8), + Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), + }) + .Select(slot => GenerateSlot(seed, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) + .AsList(); + } + + return (facility, slots); + }) + .AsList(); var facilities = facilitiesAndSlots.Select(facilityAndSlots => facilityAndSlots.facility); var slotTableSlots = facilitiesAndSlots.SelectMany(facilityAndSlots => facilityAndSlots.slots); await db.InsertAllAsync(facilities); await db.InsertAllAsync(slotTableSlots); } + private static SlotTable GenerateSlot(OpportunitySeed seed, ref int slotId, DateTime startDate, int totalUses, decimal price) + { + var requiresAdditionalDetails = Faker.Random.Bool(ProportionWithRequiresAdditionalDetails); + return new SlotTable + { + FacilityUseId = seed.Id, + Id = slotId++, + Deleted = false, + Start = startDate, + End = startDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), + MaximumUses = totalUses, + RemainingUses = totalUses, + Price = price, + Prepayment = price == 0 + ? Faker.Random.Bool() ? RequiredStatusType.Unavailable : (RequiredStatusType?)null + : Faker.Random.Bool() ? Faker.Random.Enum() : (RequiredStatusType?)null, + RequiresAttendeeValidation = Faker.Random.Bool(ProportionWithRequiresAttendeeValidation), + RequiresAdditionalDetails = requiresAdditionalDetails, + RequiredAdditionalDetails = requiresAdditionalDetails ? PickRandomAdditionalDetails() : null, + RequiresApproval = seed.RequiresApproval, + AllowsProposalAmendment = seed.RequiresApproval && Faker.Random.Bool(), + ValidFromBeforeStartDate = seed.RandomValidFromBeforeStartDate(), + LatestCancellationBeforeStartDate = RandomLatestCancellationBeforeStartDate(), + AllowCustomerCancellationFullRefund = Faker.Random.Bool() + }; + } public static async Task CreateFakeClasses(IDbConnection db) { @@ -2014,7 +1987,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }), Modified = DateTimeOffset.Now.UtcTicks }; - if (_generateIndividualFacilityUses) + if (!_facilityUseHasSlots) { facility.IndividualFacilityUses = new List { new IndividualFacilityUse { @@ -2052,7 +2025,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli AllowCustomerCancellationFullRefund = allowCustomerCancellationFullRefund, Modified = DateTimeOffset.Now.UtcTicks }; - if (_generateIndividualFacilityUses) + if (!_facilityUseHasSlots) { individualFacilityUseId = 1; slot.IndividualFacilityUseId = individualFacilityUseId; From 43ae00a8cf24dafd121be250e097b452f41f68e7 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 12:56:49 +0100 Subject: [PATCH 06/14] feat: Add IFU CI support --- .github/workflows/openactive-test-suite.yml | 2 +- .../appsettings.facilityuse-has-slots.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Examples/BookingSystem.AspNetCore/appsettings.facilityuse-has-slots.json diff --git a/.github/workflows/openactive-test-suite.yml b/.github/workflows/openactive-test-suite.yml index f72c6074..c07177fd 100644 --- a/.github/workflows/openactive-test-suite.yml +++ b/.github/workflows/openactive-test-suite.yml @@ -43,7 +43,7 @@ jobs: fail-fast: false matrix: mode: ['random', 'controlled'] - profile: ['all-features', 'single-seller', 'no-payment-reconciliation', 'no-auth', 'no-tax-calculation', 'prepayment-always-required'] + profile: ['all-features', 'single-seller', 'no-payment-reconciliation', 'no-auth', 'no-tax-calculation', 'prepayment-always-required', 'facilityuse-has-slots'] steps: - name: Checkout OpenActive.Server.NET uses: actions/checkout@v2 diff --git a/Examples/BookingSystem.AspNetCore/appsettings.facilityuse-has-slots.json b/Examples/BookingSystem.AspNetCore/appsettings.facilityuse-has-slots.json new file mode 100644 index 00000000..a7067328 --- /dev/null +++ b/Examples/BookingSystem.AspNetCore/appsettings.facilityuse-has-slots.json @@ -0,0 +1,5 @@ +{ + "FeatureFlags": { + "FacilityUseHasSlots": true + } +} From 4eeedc23e54473c42bcdf0beda234660c6fe31a4 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:03:26 +0100 Subject: [PATCH 07/14] Fix tests --- .../OpenActive.FakeDatabase.NET.Tests/FakeBookingSystemTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fakes/OpenActive.FakeDatabase.NET.Tests/FakeBookingSystemTest.cs b/Fakes/OpenActive.FakeDatabase.NET.Tests/FakeBookingSystemTest.cs index 6b5a4f69..ce0b9dda 100644 --- a/Fakes/OpenActive.FakeDatabase.NET.Tests/FakeBookingSystemTest.cs +++ b/Fakes/OpenActive.FakeDatabase.NET.Tests/FakeBookingSystemTest.cs @@ -10,7 +10,7 @@ namespace OpenActive.FakeDatabase.NET.Test public class FakeBookingSystemTest { private readonly ITestOutputHelper output; - private readonly FakeBookingSystem fakeBookingSystem = new FakeBookingSystem(); + private readonly FakeBookingSystem fakeBookingSystem = new FakeBookingSystem(false); public FakeBookingSystemTest(ITestOutputHelper output) { From 52e993440f3862044980f90267294bf223dc5cb4 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:07:00 +0100 Subject: [PATCH 08/14] Fix typo --- .../Custom/Settings/AppSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs index 30b29afa..d76b2056 100644 --- a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs @@ -4,6 +4,7 @@ public class AppSettings { public string JsonLdIdBaseUrl { get; set; } public FeatureSettings FeatureFlags { get; set; } + } /** * Note feature defaults are set here, and are used for the .NET Framework reference implementation @@ -12,5 +13,4 @@ public class FeatureSettings { public bool FacilityUseHasSlots { get; set; } = false; } - } } \ No newline at end of file From 3a81603494b1064c44b068b0bc084f8e99599595 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:13:55 +0100 Subject: [PATCH 09/14] Deploy ref impl even if framework fails --- .github/workflows/openactive-test-suite.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/openactive-test-suite.yml b/.github/workflows/openactive-test-suite.yml index c07177fd..b8d20c3b 100644 --- a/.github/workflows/openactive-test-suite.yml +++ b/.github/workflows/openactive-test-suite.yml @@ -177,7 +177,6 @@ jobs: if: ${{ github.ref == 'refs/heads/master' }} needs: - core - - framework runs-on: ubuntu-latest steps: # Checkout the repo From 07bdaa709f1bcc6027cbab4a04cd1eb552fb0e82 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:29:31 +0100 Subject: [PATCH 10/14] Fix Auth --- .../appsettings.facilityuse-has-slots.json | 5 +++++ .../BookingSystem.AspNetCore.IdentityServer/appsettings.json | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.facilityuse-has-slots.json diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.facilityuse-has-slots.json b/Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.facilityuse-has-slots.json new file mode 100644 index 00000000..a7067328 --- /dev/null +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.facilityuse-has-slots.json @@ -0,0 +1,5 @@ +{ + "FeatureFlags": { + "FacilityUseHasSlots": true + } +} diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.json b/Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.json index d9a9dfc0..7e3d649d 100644 --- a/Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.json +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.json @@ -1,4 +1,7 @@ { "Urls": "https://localhost:5003;http://localhost:5002", - "JsonLdIdBaseUrl": "https://localhost:5001" + "JsonLdIdBaseUrl": "https://localhost:5001", + "FeatureFlags": { + "FacilityUseHasSlots": false + } } \ No newline at end of file From 14369ac81842ed15860e0a6f8c2e4e6b93b40c20 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:52:48 +0100 Subject: [PATCH 11/14] Fix typo --- Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index 025eeb01..da0c4e73 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -1527,7 +1527,7 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool fa Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), IndividualFacilityUseId = ifu.Id, }) - .Select(slot => GenerateSlot(seed, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) + .Select(slot => GenerateSlot(seed, slot.IndividualFacilityUseId, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) .AsList(); } else @@ -1539,7 +1539,7 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool fa TotalUses = Faker.Random.Int(0, 8), Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), }) - .Select(slot => GenerateSlot(seed, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) + .Select(slot => GenerateSlot(seed, null, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) .AsList(); } @@ -1552,12 +1552,13 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool fa await db.InsertAllAsync(facilities); await db.InsertAllAsync(slotTableSlots); } - private static SlotTable GenerateSlot(OpportunitySeed seed, ref int slotId, DateTime startDate, int totalUses, decimal price) + private static SlotTable GenerateSlot(OpportunitySeed seed, long? individualFacilityUseId, ref int slotId, DateTime startDate, int totalUses, decimal price) { var requiresAdditionalDetails = Faker.Random.Bool(ProportionWithRequiresAdditionalDetails); return new SlotTable { FacilityUseId = seed.Id, + IndividualFacilityUseId = individualFacilityUseId, Id = slotId++, Deleted = false, Start = startDate, From dbb426242b545f6cd100f253342e079018dfcfb2 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:59:50 +0100 Subject: [PATCH 12/14] Test commit From df8d64861718db399f4038e7e8c759f0c6403a8d Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 14:06:26 +0100 Subject: [PATCH 13/14] Fix Framework --- Examples/BookingSystem.AspNetFramework/Utils/ServiceConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/BookingSystem.AspNetFramework/Utils/ServiceConfig.cs b/Examples/BookingSystem.AspNetFramework/Utils/ServiceConfig.cs index 9fb39c95..340ea85c 100644 --- a/Examples/BookingSystem.AspNetFramework/Utils/ServiceConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Utils/ServiceConfig.cs @@ -32,7 +32,7 @@ public static void Register(HttpConfiguration config) services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddSingleton(sp => EngineConfig.CreateStoreBookingEngine(appSettings, new FakeBookingSystem())); + services.AddSingleton(sp => EngineConfig.CreateStoreBookingEngine(appSettings, new FakeBookingSystem(false))); var resolver = new DependencyResolver(services.BuildServiceProvider(true)); config.DependencyResolver = resolver; From a57ff23f13c37e7843dd6e5adc3d71481c71e8e0 Mon Sep 17 00:00:00 2001 From: nickevansuk <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 15:30:22 +0100 Subject: [PATCH 14/14] Improve data distribution --- Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index da0c4e73..f30ab2e9 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -1536,7 +1536,7 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool fa .Select(_ => new { StartDate = seed.RandomStartDate(), - TotalUses = Faker.Random.Int(0, 8), + TotalUses = Faker.Random.Int(1, 8), Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price((decimal)0.5, 20)), }) .Select(slot => GenerateSlot(seed, null, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) @@ -1564,7 +1564,7 @@ private static SlotTable GenerateSlot(OpportunitySeed seed, long? individualFaci Start = startDate, End = startDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), MaximumUses = totalUses, - RemainingUses = totalUses, + RemainingUses = Faker.PickRandom(new[] { 0, totalUses, totalUses, totalUses, totalUses, totalUses, totalUses, totalUses, totalUses }), Price = price, Prepayment = price == 0 ? Faker.Random.Bool() ? RequiredStatusType.Unavailable : (RequiredStatusType?)null @@ -1625,7 +1625,7 @@ public static async Task CreateFakeClasses(IDbConnection db) .Select(_ => new { Start = seed.RandomStartDate(), - TotalSpaces = Faker.Random.Bool() ? Faker.Random.Int(0, 50) : Faker.Random.Int(0, 3) + TotalSpaces = Faker.Random.Bool() ? Faker.Random.Int(1, 50) : Faker.Random.Int(1, 3) }) .Select(occurrence => new OccurrenceTable { @@ -1635,7 +1635,7 @@ public static async Task CreateFakeClasses(IDbConnection db) Start = occurrence.Start, End = occurrence.Start + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), TotalSpaces = occurrence.TotalSpaces, - RemainingSpaces = occurrence.TotalSpaces + RemainingSpaces = Faker.PickRandom(new[] { 0, occurrence.TotalSpaces, occurrence.TotalSpaces, occurrence.TotalSpaces, occurrence.TotalSpaces, occurrence.TotalSpaces, occurrence.TotalSpaces, occurrence.TotalSpaces, occurrence.TotalSpaces }) })).SelectMany(os => os); await db.InsertAllAsync(classes);