From ecc3fb6c239d69f9eec7b9da5a118555e81f761b Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Tue, 13 Jun 2023 18:48:38 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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 })