From 6564d5e3ad71614e09366d03e7b83d1a00b2bb2c Mon Sep 17 00:00:00 2001 From: Nick Evans <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 08:56:05 +0100 Subject: [PATCH 1/5] feat: Fake places and simulation of a custom built system (#202) --- .../Feeds/FacilitiesFeeds.cs | 18 +-- .../Feeds/SessionsFeeds.cs | 36 +---- .../Settings/AppSettings.cs | 1 + .../Settings/EngineConfig.cs | 6 +- .../Stores/FacilityStore.cs | 12 +- .../Stores/SessionStore.cs | 2 +- .../appsettings.single-seller.json | 1 + .../Feeds/FacilitiesFeeds.cs | 18 +-- .../Feeds/SessionsFeeds.cs | 36 +---- .../Settings/AppSettings.cs | 1 + .../Settings/EngineConfig.cs | 6 +- .../Stores/FacilityStore.cs | 12 +- .../Stores/SessionStore.cs | 2 +- .../FakeBookingSystem.cs | 139 ++++++++++++++++-- .../Models/ClassTable.cs | 3 +- .../Models/FacilityUseTable.cs | 3 +- 16 files changed, 147 insertions(+), 149 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 96d126db..0b2d8301 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -88,23 +88,7 @@ protected override async Task>> GetRpdeItems(long? af }, IsOpenBookingAllowed = true, }, - Location = new Place - { - Name = "Fake Pond", - Address = new PostalAddress - { - StreetAddress = "1 Fake Park", - AddressLocality = "Another town", - AddressRegion = "Oxfordshire", - PostalCode = "OX1 1AA", - AddressCountry = "GB" - }, - Geo = new GeoCoordinates - { - Latitude = result.Item1.LocationLat, - Longitude = result.Item1.LocationLng - } - }, + Location = _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), Url = new Uri("https://www.example.com/a-session-age"), FacilityType = new List { new Concept diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index ad0adbad..aba2e7d8 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -171,40 +171,8 @@ protected override async Task>> GetRpdeItems(long? AllowCustomerCancellationFullRefund = result.Item1.AllowCustomerCancellationFullRefund } }, - Location = result.Item1.AttendanceMode == AttendanceMode.Online ? null : new Place - { - Name = "Fake Pond", - Address = new PostalAddress - { - StreetAddress = "1 Fake Park", - AddressLocality = "Another town", - AddressRegion = "Oxfordshire", - PostalCode = "OX1 1AA", - AddressCountry = "GB" - }, - Geo = new GeoCoordinates - { - Latitude = result.Item1.LocationLat, - Longitude = result.Item1.LocationLng, - } - }, - AffiliatedLocation = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : new Place - { - Name = "Fake Pond", - Address = new PostalAddress - { - StreetAddress = "1 Fake Park", - AddressLocality = "Another town", - AddressRegion = "Oxfordshire", - PostalCode = "OX1 1AA", - AddressCountry = "GB" - }, - Geo = new GeoCoordinates - { - Latitude = result.Item1.LocationLat, - Longitude = result.Item1.LocationLng, - } - }, + Location = result.Item1.AttendanceMode == AttendanceMode.Online ? null : _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), + AffiliatedLocation = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), Url = new Uri("https://www.example.com/a-session-age"), Activity = new List { diff --git a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs index 6ead5201..17fe707a 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs @@ -15,6 +15,7 @@ public class FeatureSettings { public bool EnableTokenAuth { get; set; } = true; public bool SingleSeller { get; set; } = false; + public bool CustomBuiltSystem { get; set; } = false; public bool PaymentReconciliationDetailValidation { get; set; } = true; public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; diff --git a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs index 3d78fc44..09bdeefe 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs @@ -176,9 +176,9 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OrganisationPlainTextDescription = "The Reference Implementation provides an example of an full conformant implementation of the OpenActive specifications.", OrganisationLogoUrl = $"{appSettings.ApplicationHostBaseUrl}/images/placeholder-logo.png".ParseUrlOrNull(), OrganisationEmail = "hello@example.com", - PlatformName = "OpenActive Reference Implementation", - PlatformUrl = "https://tutorials.openactive.io/open-booking-sdk/".ParseUrlOrNull(), - PlatformVersion = "1.0", + PlatformName = appSettings.FeatureFlags.CustomBuiltSystem ? null : "OpenActive Reference Implementation", + PlatformUrl = appSettings.FeatureFlags.CustomBuiltSystem ? null : "https://tutorials.openactive.io/open-booking-sdk/".ParseUrlOrNull(), + PlatformVersion = appSettings.FeatureFlags.CustomBuiltSystem ? null : "1.0", BackgroundImageUrl = $"{appSettings.ApplicationHostBaseUrl}/images/placeholder-dataset-site-background.jpg".ParseUrlOrNull(), DateFirstPublished = new DateTimeOffset(new DateTime(2019, 01, 14)), OpenBookingAPIBaseUrl = $"{appSettings.ApplicationHostBaseUrl}/api/openbooking".ParseUrlOrNull(), diff --git a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs index 0c806d54..7e44b71e 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs @@ -274,7 +274,7 @@ protected override async Task TriggerTestAction(OpenBookingSimulateAction simula } return; case ChangeOfLogisticsLocationSimulateAction _: - if (!await _fakeBookingSystem.Database.UpdateFacilityUseLocationLatLng(idComponents.SlotId.Value, 0.2m, 0.3m)) + if (!await _fakeBookingSystem.Database.UpdateFacilityUseLocationPlaceId(idComponents.SlotId.Value)) { throw new OpenBookingException(new UnknownOpportunityError()); } @@ -339,15 +339,7 @@ protected override async Task GetOrderItems(List { new Concept { diff --git a/Examples/BookingSystem.AspNetCore/Stores/SessionStore.cs b/Examples/BookingSystem.AspNetCore/Stores/SessionStore.cs index bd767851..42bf975b 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/SessionStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/SessionStore.cs @@ -295,7 +295,7 @@ protected override async Task TriggerTestAction(OpenBookingSimulateAction simula } return; case ChangeOfLogisticsLocationSimulateAction _: - if (!await _fakeBookingSystem.Database.UpdateSessionSeriesLocationLatLng(idComponents.ScheduledSessionId.Value, 0.2m, 0.3m)) + if (!await _fakeBookingSystem.Database.UpdateSessionSeriesLocationPlaceId(idComponents.ScheduledSessionId.Value)) { throw new OpenBookingException(new UnknownOpportunityError()); } diff --git a/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json b/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json index c1210ca3..5c4cce23 100644 --- a/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json +++ b/Examples/BookingSystem.AspNetCore/appsettings.single-seller.json @@ -1,6 +1,7 @@ { "FeatureFlags": { "SingleSeller": true, + "CustomBuiltSystem": true, "EnableTokenAuth": false } } diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs index 96d126db..0b2d8301 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs @@ -88,23 +88,7 @@ protected override async Task>> GetRpdeItems(long? af }, IsOpenBookingAllowed = true, }, - Location = new Place - { - Name = "Fake Pond", - Address = new PostalAddress - { - StreetAddress = "1 Fake Park", - AddressLocality = "Another town", - AddressRegion = "Oxfordshire", - PostalCode = "OX1 1AA", - AddressCountry = "GB" - }, - Geo = new GeoCoordinates - { - Latitude = result.Item1.LocationLat, - Longitude = result.Item1.LocationLng - } - }, + Location = _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), Url = new Uri("https://www.example.com/a-session-age"), FacilityType = new List { new Concept diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs index ad0adbad..aba2e7d8 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs @@ -171,40 +171,8 @@ protected override async Task>> GetRpdeItems(long? AllowCustomerCancellationFullRefund = result.Item1.AllowCustomerCancellationFullRefund } }, - Location = result.Item1.AttendanceMode == AttendanceMode.Online ? null : new Place - { - Name = "Fake Pond", - Address = new PostalAddress - { - StreetAddress = "1 Fake Park", - AddressLocality = "Another town", - AddressRegion = "Oxfordshire", - PostalCode = "OX1 1AA", - AddressCountry = "GB" - }, - Geo = new GeoCoordinates - { - Latitude = result.Item1.LocationLat, - Longitude = result.Item1.LocationLng, - } - }, - AffiliatedLocation = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : new Place - { - Name = "Fake Pond", - Address = new PostalAddress - { - StreetAddress = "1 Fake Park", - AddressLocality = "Another town", - AddressRegion = "Oxfordshire", - PostalCode = "OX1 1AA", - AddressCountry = "GB" - }, - Geo = new GeoCoordinates - { - Latitude = result.Item1.LocationLat, - Longitude = result.Item1.LocationLng, - } - }, + Location = result.Item1.AttendanceMode == AttendanceMode.Online ? null : _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), + AffiliatedLocation = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), Url = new Uri("https://www.example.com/a-session-age"), Activity = new List { diff --git a/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs index 6ead5201..17fe707a 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs @@ -15,6 +15,7 @@ public class FeatureSettings { public bool EnableTokenAuth { get; set; } = true; public bool SingleSeller { get; set; } = false; + public bool CustomBuiltSystem { get; set; } = false; public bool PaymentReconciliationDetailValidation { get; set; } = true; public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; diff --git a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs index 3d78fc44..09bdeefe 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs @@ -176,9 +176,9 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OrganisationPlainTextDescription = "The Reference Implementation provides an example of an full conformant implementation of the OpenActive specifications.", OrganisationLogoUrl = $"{appSettings.ApplicationHostBaseUrl}/images/placeholder-logo.png".ParseUrlOrNull(), OrganisationEmail = "hello@example.com", - PlatformName = "OpenActive Reference Implementation", - PlatformUrl = "https://tutorials.openactive.io/open-booking-sdk/".ParseUrlOrNull(), - PlatformVersion = "1.0", + PlatformName = appSettings.FeatureFlags.CustomBuiltSystem ? null : "OpenActive Reference Implementation", + PlatformUrl = appSettings.FeatureFlags.CustomBuiltSystem ? null : "https://tutorials.openactive.io/open-booking-sdk/".ParseUrlOrNull(), + PlatformVersion = appSettings.FeatureFlags.CustomBuiltSystem ? null : "1.0", BackgroundImageUrl = $"{appSettings.ApplicationHostBaseUrl}/images/placeholder-dataset-site-background.jpg".ParseUrlOrNull(), DateFirstPublished = new DateTimeOffset(new DateTime(2019, 01, 14)), OpenBookingAPIBaseUrl = $"{appSettings.ApplicationHostBaseUrl}/api/openbooking".ParseUrlOrNull(), diff --git a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs index 0c806d54..7e44b71e 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs @@ -274,7 +274,7 @@ protected override async Task TriggerTestAction(OpenBookingSimulateAction simula } return; case ChangeOfLogisticsLocationSimulateAction _: - if (!await _fakeBookingSystem.Database.UpdateFacilityUseLocationLatLng(idComponents.SlotId.Value, 0.2m, 0.3m)) + if (!await _fakeBookingSystem.Database.UpdateFacilityUseLocationPlaceId(idComponents.SlotId.Value)) { throw new OpenBookingException(new UnknownOpportunityError()); } @@ -339,15 +339,7 @@ protected override async Task GetOrderItems(List { new Concept { diff --git a/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs index bd767851..42bf975b 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs @@ -295,7 +295,7 @@ protected override async Task TriggerTestAction(OpenBookingSimulateAction simula } return; case ChangeOfLogisticsLocationSimulateAction _: - if (!await _fakeBookingSystem.Database.UpdateSessionSeriesLocationLatLng(idComponents.ScheduledSessionId.Value, 0.2m, 0.3m)) + if (!await _fakeBookingSystem.Database.UpdateSessionSeriesLocationPlaceId(idComponents.ScheduledSessionId.Value)) { throw new OpenBookingException(new UnknownOpportunityError()); } diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index 7d24292e..f2bebf0c 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -283,7 +283,7 @@ public async Task UpdateFacilitySlotStartAndEndTimeByPeriodInMins(long slo /// /// /// - public async Task UpdateFacilityUseLocationLatLng(long slotId, decimal newLat, decimal newLng) + public async Task UpdateFacilityUseLocationPlaceId(long slotId) { using (var db = await Mem.Database.OpenAsync()) { @@ -295,8 +295,8 @@ public async Task UpdateFacilityUseLocationLatLng(long slotId, decimal new if (facilityUse == null) return false; - facilityUse.LocationLat = newLat; - facilityUse.LocationLng = newLng; + // Round-robin to a different place + facilityUse.PlaceId = (facilityUse.PlaceId + 1) % 3 + 1; facilityUse.Modified = DateTimeOffset.Now.UtcTicks; await db.UpdateAsync(facilityUse); return true; @@ -357,7 +357,7 @@ public async Task UpdateScheduledSessionStartAndEndTimeByPeriodInMins(long /// /// /// - public async Task UpdateSessionSeriesLocationLatLng(long occurrenceId, decimal newLat, decimal newLng) + public async Task UpdateSessionSeriesLocationPlaceId(long occurrenceId) { using (var db = await Mem.Database.OpenAsync()) { @@ -369,8 +369,8 @@ public async Task UpdateSessionSeriesLocationLatLng(long occurrenceId, dec if (classInstance == null) return false; - classInstance.LocationLat = newLat; - classInstance.LocationLng = newLng; + // Round-robin to a different place + classInstance.PlaceId = (classInstance.PlaceId + 1) % 3 + 1; classInstance.Modified = DateTimeOffset.Now.UtcTicks; await db.UpdateAsync(classInstance); return true; @@ -638,6 +638,118 @@ await db.InsertAsync(new OrderTable } } + public OpenActive.NET.Place GetPlaceById(long placeId) + { + // Three hardcoded fake places + switch (placeId) + { + case 1: + return new OpenActive.NET.Place + { + Identifier = 1, + Name = "Post-ercise Plaza", + Description = "Sorting Out Your Fitness One Parcel Lift at a Time! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + Address = new OpenActive.NET.PostalAddress + { + StreetAddress = "Kings Mead House", + AddressLocality = "Oxford", + AddressRegion = "Oxfordshire", + PostalCode = "OX1 1AA", + AddressCountry = "GB" + }, + Geo = new OpenActive.NET.GeoCoordinates + { + Latitude = (decimal?)51.7502, + Longitude = (decimal?)-1.2674 + }, + Image = new List { + new OpenActive.NET.ImageObject + { + Url = new Uri("https://upload.wikimedia.org/wikipedia/commons/e/e5/Oxford_StAldates_PostOffice.jpg") + }, + }, + Telephone = "01865 000001", + Url = new Uri("https://en.wikipedia.org/wiki/Post_Office_Limited"), + AmenityFeature = new List + { + new OpenActive.NET.ChangingFacilities { Name = "Changing Facilities", Value = true }, + new OpenActive.NET.Showers { Name = "Showers", Value = true }, + new OpenActive.NET.Lockers { Name = "Lockers", Value = true }, + new OpenActive.NET.Towels { Name = "Towels", Value = false }, + new OpenActive.NET.Creche { Name = "Creche", Value = false }, + new OpenActive.NET.Parking { Name = "Parking", Value = false } + } + }; + case 2: + return new OpenActive.NET.Place + { + Identifier = 2, + Name = "Premier Lifters", + Description = "Where your Fitness Goals are Always Inn-Sight. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + Address = new OpenActive.NET.PostalAddress + { + StreetAddress = "Greyfriars Court, Paradise Square", + AddressLocality = "Oxford", + AddressRegion = "Oxfordshire", + PostalCode = "OX1 1BB", + AddressCountry = "GB" + }, + Geo = new OpenActive.NET.GeoCoordinates + { + Latitude = (decimal?)51.7504933, + Longitude = (decimal?)-1.2620685 + }, + Image = new List { + new OpenActive.NET.ImageObject + { + Url = new Uri("https://upload.wikimedia.org/wikipedia/commons/5/53/Cambridge_Orchard_Park_Premier_Inn.jpg") + }, + }, + Telephone = "01865 000002", + Url = new Uri("https://en.wikipedia.org/wiki/Premier_Inn"), + AmenityFeature = new List + { + new OpenActive.NET.ChangingFacilities { Name = "Changing Facilities", Value = false }, + new OpenActive.NET.Showers { Name = "Showers", Value = false }, + new OpenActive.NET.Lockers { Name = "Lockers", Value = false }, + new OpenActive.NET.Towels { Name = "Towels", Value = true }, + new OpenActive.NET.Creche { Name = "Creche", Value = true }, + new OpenActive.NET.Parking { Name = "Parking", Value = true } + } + }; + case 3: + return new OpenActive.NET.Place + { + Identifier = 3, + Name = "Stroll & Stretch", + Description = "Casual Calisthenics in the Heart of Commerce. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + Address = new OpenActive.NET.PostalAddress + { + StreetAddress = "Norfolk Street", + AddressLocality = "Oxford", + AddressRegion = "Oxfordshire", + PostalCode = "OX1 1UU", + AddressCountry = "GB" + }, + Geo = new OpenActive.NET.GeoCoordinates + { + Latitude = (decimal?)51.749826, + Longitude = (decimal?)-1.261492 + }, + Image = new List { + new OpenActive.NET.ImageObject + { + Url = new Uri("https://upload.wikimedia.org/wikipedia/commons/2/28/Westfield_Garden_State_Plaza_-_panoramio.jpg") + }, + }, + Telephone = "01865 000003", + Url = new Uri("https://en.wikipedia.org/wiki/Shopping_center"), + }; + default: + return null; + } + } + public async Task<(bool, FacilityUseTable, SlotTable, BookedOrderItemInfo)> GetSlotAndBookedOrderItemInfoBySlotId(Guid uuid, long? slotId) { using (var db = await Mem.Database.OpenAsync()) @@ -1381,7 +1493,8 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db) 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 + 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 }) }) .AsList(); @@ -1459,7 +1572,8 @@ public static async Task CreateFakeClasses(IDbConnection db) SellerId = Faker.Random.Bool(0.8f) ? Faker.Random.Long(1, 2) : Faker.Random.Long(3, 5), // distribution: 80% 1-2, 20% 3-5 ValidFromBeforeStartDate = @class.ValidFromBeforeStartDate, AttendanceMode = Faker.PickRandom(), - AllowCustomerCancellationFullRefund = Faker.Random.Bool() + AllowCustomerCancellationFullRefund = Faker.Random.Bool(), + PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }) }; }) .AsList(); @@ -1743,8 +1857,6 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli RequiredStatusType? prepayment = null, bool requiresAttendeeValidation = false, bool requiresAdditionalDetails = false, - decimal locationLat = 0.1m, - decimal locationLng = 0.1m, bool isOnlineOrMixedAttendanceMode = false, bool allowProposalAmendment = false, bool inPast = false) @@ -1775,8 +1887,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli RequiresAdditionalDetails = requiresAdditionalDetails, RequiredAdditionalDetails = requiresAdditionalDetails ? PickRandomAdditionalDetails() : null, AllowsProposalAmendment = allowProposalAmendment, - LocationLat = locationLat, - LocationLng = locationLng, + PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }), AttendanceMode = isOnlineOrMixedAttendanceMode ? Faker.PickRandom(new[] { AttendanceMode.Mixed, AttendanceMode.Online }) : AttendanceMode.Offline, AllowCustomerCancellationFullRefund = allowCustomerCancellationFullRefund, Modified = DateTimeOffset.Now.UtcTicks @@ -1815,7 +1926,6 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli RequiredStatusType? prepayment = null, bool requiresAttendeeValidation = false, bool requiresAdditionalDetails = false, - decimal locationLat = 0.1m, decimal locationLng = 0.1m, bool allowProposalAmendment = false, bool inPast = false) @@ -1832,8 +1942,7 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli Deleted = false, Name = title, SellerId = sellerId ?? 1, - LocationLat = locationLat, - LocationLng = locationLng, + PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }), Modified = DateTimeOffset.Now.UtcTicks }; await db.SaveAsync(facility); diff --git a/Fakes/OpenActive.FakeDatabase.NET/Models/ClassTable.cs b/Fakes/OpenActive.FakeDatabase.NET/Models/ClassTable.cs index daac55f9..2395bc7b 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/Models/ClassTable.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/Models/ClassTable.cs @@ -21,8 +21,7 @@ public class ClassTable : Table public bool RequiresApproval { get; set; } public TimeSpan? ValidFromBeforeStartDate { get; set; } public TimeSpan? LatestCancellationBeforeStartDate { get; set; } - public decimal LocationLat { get; set; } - public decimal LocationLng { get; set; } + public long PlaceId { get; set; } public AttendanceMode AttendanceMode { get; set; } public bool AllowsProposalAmendment { get; set; } } diff --git a/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs b/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs index c875db32..6e1f986e 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs @@ -11,7 +11,6 @@ public class FacilityUseTable : Table public SellerTable SellerTable { get; set; } [ForeignKey(typeof(SellerTable), OnDelete = "CASCADE")] public long SellerId { get; set; } // Provider - public decimal LocationLat { get; set; } - public decimal LocationLng { get; set; } + public long PlaceId { get; set; } } } \ No newline at end of file From 1cbc00fb0526846765393a9d76c83d8834f7b84b Mon Sep 17 00:00:00 2001 From: Luke Winship Date: Wed, 2 Aug 2023 14:17:28 +0100 Subject: [PATCH 2/5] feat: Use env vars to override generated activity / facilityType (#198) --- Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs | 7 +++++-- Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs | 7 +++++-- Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs | 7 +++++-- .../BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs | 7 +++++-- .../BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs | 7 +++++-- .../BookingSystem.AspNetFramework/Stores/FacilityStore.cs | 7 +++++-- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 0b2d8301..71378941 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -26,6 +26,9 @@ public AcmeFacilityUseRpdeGenerator(AppSettings appSettings, FakeBookingSystem f protected override async Task>> GetRpdeItems(long? afterTimestamp, long? afterId) { + var facilityTypeId = Environment.GetEnvironmentVariable("FACILITY_TYPE_ID") ?? "https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"; + var facilityTypePrefLabel = Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL") ?? "Squash Court"; + using (var db = _fakeBookingSystem.Database.Mem.Database.Open()) { var q = db.From() @@ -93,8 +96,8 @@ protected override async Task>> GetRpdeItems(long? af FacilityType = new List { new Concept { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", + Id = new Uri(facilityTypeId), + PrefLabel = facilityTypePrefLabel, InScheme = new Uri("https://openactive.io/facility-types") } } diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index aba2e7d8..b803b921 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -85,6 +85,9 @@ public AcmeSessionSeriesRpdeGenerator(AppSettings appSettings, FakeBookingSystem protected override async Task>> GetRpdeItems(long? afterTimestamp, long? afterId) { + var activityId = Environment.GetEnvironmentVariable("ACTIVITY_ID") ?? "https://openactive.io/activity-list#c07d63a0-8eb9-4602-8bcc-23be6deb8f83"; + var activityPrefLabel = Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL") ?? "Jet Skiing"; + using (var db = _fakeBookingSystem.Database.Mem.Database.Open()) { var q = db.From() @@ -178,8 +181,8 @@ protected override async Task>> GetRpdeItems(long? { new Concept { - Id = new Uri("https://openactive.io/activity-list#c07d63a0-8eb9-4602-8bcc-23be6deb8f83"), - PrefLabel = "Jet Skiing", + Id = new Uri(activityId), + PrefLabel = activityPrefLabel, InScheme = new Uri("https://openactive.io/activity-list") } } diff --git a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs index 7e44b71e..c8f2a311 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs @@ -288,6 +288,9 @@ protected override async Task TriggerTestAction(OpenBookingSimulateAction simula // Similar to the RPDE logic, this needs to render and return an new hypothetical OrderItem from the database based on the supplied opportunity IDs protected override async Task GetOrderItems(List> orderItemContexts, StoreBookingFlowContext flowContext, OrderStateContext stateContext) { + var facilityTypeId = Environment.GetEnvironmentVariable("FACILITY_TYPE_ID") ?? "https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"; + var facilityTypePrefLabel = Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL") ?? "Squash Court"; + // Note the implementation of this method must also check that this OrderItem is from the Seller specified by context.SellerId (this is not required if using a Single Seller) // Additionally this method must check that there are enough spaces in each entry @@ -343,8 +346,8 @@ protected override async Task GetOrderItems(List { new Concept { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", + Id = new Uri(facilityTypeId), + PrefLabel = facilityTypePrefLabel, InScheme = new Uri("https://openactive.io/facility-types") } } diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs index 0b2d8301..71378941 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs @@ -26,6 +26,9 @@ public AcmeFacilityUseRpdeGenerator(AppSettings appSettings, FakeBookingSystem f protected override async Task>> GetRpdeItems(long? afterTimestamp, long? afterId) { + var facilityTypeId = Environment.GetEnvironmentVariable("FACILITY_TYPE_ID") ?? "https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"; + var facilityTypePrefLabel = Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL") ?? "Squash Court"; + using (var db = _fakeBookingSystem.Database.Mem.Database.Open()) { var q = db.From() @@ -93,8 +96,8 @@ protected override async Task>> GetRpdeItems(long? af FacilityType = new List { new Concept { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", + Id = new Uri(facilityTypeId), + PrefLabel = facilityTypePrefLabel, InScheme = new Uri("https://openactive.io/facility-types") } } diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs index aba2e7d8..b803b921 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs @@ -85,6 +85,9 @@ public AcmeSessionSeriesRpdeGenerator(AppSettings appSettings, FakeBookingSystem protected override async Task>> GetRpdeItems(long? afterTimestamp, long? afterId) { + var activityId = Environment.GetEnvironmentVariable("ACTIVITY_ID") ?? "https://openactive.io/activity-list#c07d63a0-8eb9-4602-8bcc-23be6deb8f83"; + var activityPrefLabel = Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL") ?? "Jet Skiing"; + using (var db = _fakeBookingSystem.Database.Mem.Database.Open()) { var q = db.From() @@ -178,8 +181,8 @@ protected override async Task>> GetRpdeItems(long? { new Concept { - Id = new Uri("https://openactive.io/activity-list#c07d63a0-8eb9-4602-8bcc-23be6deb8f83"), - PrefLabel = "Jet Skiing", + Id = new Uri(activityId), + PrefLabel = activityPrefLabel, InScheme = new Uri("https://openactive.io/activity-list") } } diff --git a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs index 7e44b71e..c8f2a311 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs @@ -288,6 +288,9 @@ protected override async Task TriggerTestAction(OpenBookingSimulateAction simula // Similar to the RPDE logic, this needs to render and return an new hypothetical OrderItem from the database based on the supplied opportunity IDs protected override async Task GetOrderItems(List> orderItemContexts, StoreBookingFlowContext flowContext, OrderStateContext stateContext) { + var facilityTypeId = Environment.GetEnvironmentVariable("FACILITY_TYPE_ID") ?? "https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"; + var facilityTypePrefLabel = Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL") ?? "Squash Court"; + // Note the implementation of this method must also check that this OrderItem is from the Seller specified by context.SellerId (this is not required if using a Single Seller) // Additionally this method must check that there are enough spaces in each entry @@ -343,8 +346,8 @@ protected override async Task GetOrderItems(List { new Concept { - Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), - PrefLabel = "Squash Court", + Id = new Uri(facilityTypeId), + PrefLabel = facilityTypePrefLabel, InScheme = new Uri("https://openactive.io/facility-types") } } From 92bcb978c26b75134cbad15dbd83de0004b47576 Mon Sep 17 00:00:00 2001 From: civsiv Date: Wed, 2 Aug 2023 14:21:09 +0100 Subject: [PATCH 3/5] feat: add try-catch to throw extra InvalidOpportunityOrOfferIdError (#196) --- .../StoreBookingEngine/StoreBookingEngine.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs b/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs index d6185e8b..20606a7a 100644 --- a/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs +++ b/OpenActive.Server.NET/StoreBookingEngine/StoreBookingEngine.cs @@ -489,7 +489,17 @@ private List GetOrderItemContexts(List sourceOrder new IncompleteOrderItemError(), "acceptedOffer @id was not provided"); } - var idComponents = base.ResolveOpportunityID(orderedItemId, acceptedOfferId); + IBookableIdComponents idComponents; + try + { + + idComponents = base.ResolveOpportunityID(orderedItemId, acceptedOfferId); + } + catch (RequiredBaseUrlMismatchException e) + { + throw new OpenBookingException(new InvalidOpportunityOrOfferIdError(), $"Opportunity @id or Offer @id does not originate from this system, {e.Message}. Opportunity @id '{orderedItemId}'; Offer @id '{acceptedOfferId}'"); + } + if (idComponents == null) { From d643a022e729a1efc240a129d0f8e6ba875e3ede Mon Sep 17 00:00:00 2001 From: Nick Evans <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:25:41 +0100 Subject: [PATCH 4/5] feat: Add IFU CI support (#204) --- .github/workflows/openactive-test-suite.yml | 3 +- .../Custom/Settings/AppSettings.cs | 9 + .../Startup.cs | 2 +- .../appsettings.facilityuse-has-slots.json | 5 + .../appsettings.json | 5 +- .../Feeds/FacilitiesFeeds.cs | 33 +- .../IdComponents/FacilityOpportunity.cs | 5 +- .../Settings/AppSettings.cs | 1 + .../Settings/EngineConfig.cs | 69 +- Examples/BookingSystem.AspNetCore/Startup.cs | 2 +- .../Stores/FacilityStore.cs | 118 +-- .../appsettings.facilityuse-has-slots.json | 5 + .../Feeds/FacilitiesFeeds.cs | 33 +- .../IdComponents/FacilityOpportunity.cs | 5 +- .../Settings/AppSettings.cs | 1 + .../Settings/EngineConfig.cs | 69 +- .../Stores/FacilityStore.cs | 120 +-- .../Utils/ServiceConfig.cs | 2 +- .../BookingSystem.AspNetFramework/Web.config | 748 +++++++++--------- .../FakeBookingSystemTest.cs | 2 +- .../FakeBookingSystem.cs | 164 ++-- .../Models/FacilityUseTable.cs | 41 +- .../Models/SlotTable.cs | 2 + .../IdTransforms/IdTemplate.cs | 2 +- 24 files changed, 850 insertions(+), 596 deletions(-) create mode 100644 Examples/BookingSystem.AspNetCore.IdentityServer/appsettings.facilityuse-has-slots.json 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..b8d20c3b 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 @@ -177,7 +177,6 @@ jobs: if: ${{ github.ref == 'refs/heads/master' }} needs: - core - - framework runs-on: ubuntu-latest steps: # Checkout the repo diff --git a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs index afc12089..d76b2056 100644 --- a/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore.IdentityServer/Custom/Settings/AppSettings.cs @@ -3,5 +3,14 @@ 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 + */ + 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 45539b1e..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(); + services.AddSingleton(x => new FakeBookingSystem(AppSettings.FeatureFlags.FacilityUseHasSlots)); var builder = services.AddIdentityServer(options => { 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 diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 71378941..e348d1e9 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -100,7 +100,17 @@ protected override async Task>> GetRpdeItems(long? af PrefLabel = facilityTypePrefLabel, 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, } }); @@ -136,7 +146,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => new RpdeItem { - Kind = RpdeKind.FacilityUseSlot, + Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, Id = x.Id, Modified = x.Modified, State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, @@ -147,14 +157,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 = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, }), - FacilityUse = RenderOpportunityId(new FacilityOpportunity + FacilityUse = _appSettings.FeatureFlags.FacilityUseHasSlots ? + RenderOpportunityId(new FacilityOpportunity { OpportunityType = OpportunityType.FacilityUse, FacilityUseId = x.FacilityUseId + }) + : RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = x.IndividualFacilityUseId, + FacilityUseId = x.FacilityUseId, }), Identifier = x.Id, StartDate = (DateTimeOffset)x.Start, @@ -167,9 +185,10 @@ protected override async Task>> GetRpdeItems(long? afterTime Id = RenderOfferId(new FacilityOpportunity { OfferId = 0, - OpportunityType = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? 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/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs index 17fe707a..aa120720 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs @@ -19,6 +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 FacilityUseHasSlots { get; set; } = false; } public class PaymentSettings diff --git a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs index 09bdeefe..5dafa9fa 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/EngineConfig.cs @@ -12,6 +12,51 @@ public static class EngineConfig { public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSettings, FakeBookingSystem fakeBookingSystem) { + var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.FacilityUseHasSlots ? + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + 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.FacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + : + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + 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, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + ; + return new StoreBookingEngine( new BookingEngineSettings { @@ -38,24 +83,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 @@ -148,7 +177,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OpportunityType.FacilityUse, new AcmeFacilityUseRpdeGenerator(appSettings, fakeBookingSystem) }, { - OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) + appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) } }, @@ -253,7 +282,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting new SessionStore(appSettings, fakeBookingSystem), new List { OpportunityType.ScheduledSession } }, { - new FacilityStore(appSettings, fakeBookingSystem), new List { 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 5b995fc2..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())); + 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 c8f2a311..8737ba2e 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs @@ -41,12 +41,14 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -57,7 +59,7 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -66,7 +68,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -78,7 +80,8 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -92,7 +95,7 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -103,7 +106,7 @@ protected override async Task CreateOpportunityWithinTestDa } 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", @@ -113,7 +116,7 @@ protected override async Task CreateOpportunityWithinTestDa prepayment: RequiredStatusType.Optional); 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", @@ -123,7 +126,7 @@ protected override async Task CreateOpportunityWithinTestDa prepayment: RequiredStatusType.Unavailable); 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", @@ -133,7 +136,7 @@ protected override async Task CreateOpportunityWithinTestDa prepayment: RequiredStatusType.Required); 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", @@ -142,7 +145,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -151,7 +154,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -160,7 +163,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -169,7 +172,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -178,7 +181,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -187,7 +190,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -197,7 +200,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresAttendeeValidation: true); 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", @@ -207,7 +210,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresAdditionalDetails: true); 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", @@ -217,7 +220,7 @@ protected override async Task CreateOpportunityWithinTestDa allowProposalAmendment: true); 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", @@ -227,7 +230,7 @@ protected override async Task CreateOpportunityWithinTestDa allowCustomerCancellationFullRefund: false); 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", @@ -245,7 +248,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"); @@ -307,6 +311,47 @@ protected override async Task GetOrderItems(List { + new Concept + { + Id = new Uri(facilityTypeId), + PrefLabel = facilityTypePrefLabel, + InScheme = new Uri("https://openactive.io/facility-types") + } + } + }; + + FacilityUse slotParent; + if (slot.IndividualFacilityUseId != null) + { + var individualFacilityUse = facility.IndividualFacilityUses.Find(ifu => 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 = facilityUse, + }; + } + else + { + slotParent = facilityUse; + } + return new { OrderItem = new OrderItem @@ -329,29 +374,12 @@ protected override async Task GetOrderItems(List { - new Concept - { - Id = new Uri(facilityTypeId), - PrefLabel = facilityTypePrefLabel, - InScheme = new Uri("https://openactive.io/facility-types") - } - } - }, + SlotId = slot.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? slot.IndividualFacilityUseId : null, + }), + FacilityUse = slotParent, StartDate = (DateTimeOffset)slot.Start, EndDate = (DateTimeOffset)slot.End, MaximumUses = slot.MaximumUses, @@ -419,7 +447,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) { @@ -491,7 +519,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"); } @@ -554,7 +582,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.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 + } +} diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs index 71378941..e348d1e9 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs @@ -100,7 +100,17 @@ protected override async Task>> GetRpdeItems(long? af PrefLabel = facilityTypePrefLabel, 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, } }); @@ -136,7 +146,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => new RpdeItem { - Kind = RpdeKind.FacilityUseSlot, + Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, Id = x.Id, Modified = x.Modified, State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, @@ -147,14 +157,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 = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, }), - FacilityUse = RenderOpportunityId(new FacilityOpportunity + FacilityUse = _appSettings.FeatureFlags.FacilityUseHasSlots ? + RenderOpportunityId(new FacilityOpportunity { OpportunityType = OpportunityType.FacilityUse, FacilityUseId = x.FacilityUseId + }) + : RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.IndividualFacilityUse, + IndividualFacilityUseId = x.IndividualFacilityUseId, + FacilityUseId = x.FacilityUseId, }), Identifier = x.Id, StartDate = (DateTimeOffset)x.Start, @@ -167,9 +185,10 @@ protected override async Task>> GetRpdeItems(long? afterTime Id = RenderOfferId(new FacilityOpportunity { OfferId = 0, - OpportunityType = OpportunityType.FacilityUseSlot, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, FacilityUseId = x.FacilityUseId, - SlotId = x.Id + SlotId = x.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? 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 17fe707a..aa120720 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/AppSettings.cs @@ -19,6 +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 FacilityUseHasSlots { get; set; } = false; } public class PaymentSettings diff --git a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs index 09bdeefe..5dafa9fa 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs @@ -12,6 +12,51 @@ public static class EngineConfig { public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSettings, FakeBookingSystem fakeBookingSystem) { + var facilityBookablePaidIdTemplate = appSettings.FeatureFlags.FacilityUseHasSlots ? + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + 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.FacilityUse, + AssignedFeed = OpportunityType.FacilityUse, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + : + new BookablePairIdTemplate( + // Opportunity + new OpportunityIdConfiguration + { + 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, + OpportunityIdTemplate = "{+BaseUrl}/facility-uses/{FacilityUseId}" + }) + ; + return new StoreBookingEngine( new BookingEngineSettings { @@ -38,24 +83,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 @@ -148,7 +177,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting OpportunityType.FacilityUse, new AcmeFacilityUseRpdeGenerator(appSettings, fakeBookingSystem) }, { - OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) + appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, new AcmeFacilityUseSlotRpdeGenerator(appSettings,fakeBookingSystem) } }, @@ -253,7 +282,7 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting new SessionStore(appSettings, fakeBookingSystem), new List { OpportunityType.ScheduledSession } }, { - new FacilityStore(appSettings, fakeBookingSystem), new List { 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 c8f2a311..142c459f 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; @@ -41,12 +41,14 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -57,7 +59,7 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -66,7 +68,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -78,7 +80,8 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -92,7 +95,7 @@ protected override async Task CreateOpportunityWithinTestDa 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", @@ -103,7 +106,7 @@ protected override async Task CreateOpportunityWithinTestDa } 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", @@ -113,7 +116,7 @@ protected override async Task CreateOpportunityWithinTestDa prepayment: RequiredStatusType.Optional); 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", @@ -123,7 +126,7 @@ protected override async Task CreateOpportunityWithinTestDa prepayment: RequiredStatusType.Unavailable); 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", @@ -133,7 +136,7 @@ protected override async Task CreateOpportunityWithinTestDa prepayment: RequiredStatusType.Required); 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", @@ -142,7 +145,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -151,7 +154,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -160,7 +163,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -169,7 +172,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -178,7 +181,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -187,7 +190,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresApproval); 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", @@ -197,7 +200,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresAttendeeValidation: true); 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", @@ -207,7 +210,7 @@ protected override async Task CreateOpportunityWithinTestDa requiresAdditionalDetails: true); 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", @@ -217,7 +220,7 @@ protected override async Task CreateOpportunityWithinTestDa allowProposalAmendment: true); 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", @@ -227,7 +230,7 @@ protected override async Task CreateOpportunityWithinTestDa allowCustomerCancellationFullRefund: false); 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", @@ -245,7 +248,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"); @@ -307,6 +311,47 @@ protected override async Task GetOrderItems(List { + new Concept + { + Id = new Uri(facilityTypeId), + PrefLabel = facilityTypePrefLabel, + InScheme = new Uri("https://openactive.io/facility-types") + } + } + }; + + FacilityUse slotParent; + if (slot.IndividualFacilityUseId != null) + { + var individualFacilityUse = facility.IndividualFacilityUses.Find(ifu => 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 = facilityUse, + }; + } + else + { + slotParent = facilityUse; + } + return new { OrderItem = new OrderItem @@ -329,29 +374,12 @@ protected override async Task GetOrderItems(List { - new Concept - { - Id = new Uri(facilityTypeId), - PrefLabel = facilityTypePrefLabel, - InScheme = new Uri("https://openactive.io/facility-types") - } - } - }, + SlotId = slot.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? slot.IndividualFacilityUseId : null, + }), + FacilityUse = slotParent, StartDate = (DateTimeOffset)slot.Start, EndDate = (DateTimeOffset)slot.End, MaximumUses = slot.MaximumUses, @@ -419,7 +447,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) { @@ -491,7 +519,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"); } @@ -554,7 +582,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/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; 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.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) { diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index f2bebf0c..f30ab2e9 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 { @@ -22,9 +23,9 @@ namespace OpenActive.FakeDatabase.NET public class FakeBookingSystem { public FakeDatabase Database { get; set; } - public FakeBookingSystem() + public FakeBookingSystem(bool facilityUseHasSlots) { - Database = FakeDatabase.GetPrepopulatedFakeDatabase().Result; + Database = FakeDatabase.GetPrepopulatedFakeDatabase(facilityUseHasSlots).Result; } } @@ -145,11 +146,16 @@ public class FakeDatabase { private const float ProportionWithRequiresAttendeeValidation = 1f / 10; private const float ProportionWithRequiresAdditionalDetails = 1f / 10; - + private bool _facilityUseHasSlots; public readonly InMemorySQLite Mem = new InMemorySQLite(); private static readonly Faker Faker = new Faker(); + public FakeDatabase(bool facilityUseHasSlots) + { + _facilityUseHasSlots = facilityUseHasSlots; + } + static FakeDatabase() { Randomizer.Seed = new Random((int)(DateTime.Today - new DateTime(1970, 1, 1)).TotalDays); @@ -1463,9 +1469,9 @@ public static async Task RecalculateSpaces(IDbConnection db, IEnumerable o } } - public static async Task GetPrepopulatedFakeDatabase() + public static async Task GetPrepopulatedFakeDatabase(bool facilityUseHasSlots) { - var database = new FakeDatabase(); + var database = new FakeDatabase(facilityUseHasSlots); using (var db = await database.Mem.Database.OpenAsync()) using (var transaction = db.OpenTransaction(IsolationLevel.Serializable)) { @@ -1473,70 +1479,105 @@ public static async Task GetPrepopulatedFakeDatabase() await CreateSellers(db); await CreateSellerUsers(db); await CreateFakeClasses(db); - await CreateFakeFacilitiesAndSlots(db); + 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); transaction.Commit(); + } return database; } - private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db) + private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool facilityUseHasSlots) { var opportunitySeeds = GenerateOpportunitySeedDistribution(OpportunityCount); - var facilities = opportunitySeeds - .Select(seed => new FacilityUseTable + 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")}"; + var facility = 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 + 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 }) - }) - .AsList(); + }; - var slotId = 0; - var slots = opportunitySeeds.Select(seed => - Enumerable.Range(0, 10) - .Select(_ => new + // 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 = Faker.Random.Int(0, 8), - Price = decimal.Parse(Faker.Random.Bool() ? "0.00" : Faker.Commerce.Price(0, 20)), + 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 + .Select(slot => GenerateSlot(seed, slot.IndividualFacilityUseId, ref slotId, slot.StartDate, slot.TotalUses, slot.Price)) + .AsList(); + } + else + { + slots = Enumerable.Range(0, 10) + .Select(_ => new { - 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); + StartDate = seed.RandomStartDate(), + 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)) + .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); + } + 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, + End = startDate + TimeSpan.FromMinutes(Faker.Random.Int(30, 360)), + MaximumUses = 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 + : 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) @@ -1547,7 +1588,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 }) @@ -1584,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 { @@ -1594,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); @@ -1913,7 +1954,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, @@ -1928,7 +1969,8 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli bool requiresAdditionalDetails = false, decimal locationLng = 0.1m, bool allowProposalAmendment = false, - bool inPast = false) + bool inPast = false + ) { var startTime = DateTime.Now.AddDays(inPast ? -1 : 1); var endTime = DateTime.Now.AddDays(inPast ? -1 : 1).AddHours(1); @@ -1936,6 +1978,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, @@ -1945,8 +1988,19 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }), Modified = DateTimeOffset.Now.UtcTicks }; + if (!_facilityUseHasSlots) + { + 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, @@ -1972,11 +2026,17 @@ public async Task RemoveAllGrants(string subjectId, string sessionId, string cli AllowCustomerCancellationFullRefund = allowCustomerCancellationFullRefund, Modified = DateTimeOffset.Now.UtcTicks }; + if (!_facilityUseHasSlots) + { + 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 6e1f986e..e8ef94af 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/Models/FacilityUseTable.cs @@ -1,16 +1,27 @@ -using ServiceStack.DataAnnotations; - -namespace OpenActive.FakeDatabase.NET -{ - public class FacilityUseTable : Table - { - public string TestDatasetIdentifier { get; set; } - public string Name { get; set; } - public string Description { get; set; } - [Reference] - public SellerTable SellerTable { get; set; } - [ForeignKey(typeof(SellerTable), OnDelete = "CASCADE")] - public long SellerId { get; set; } // Provider - public long PlaceId { get; set; } - } +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; } + public string Name { get; set; } + public string Description { get; set; } + [Reference] + public SellerTable SellerTable { get; set; } + [ForeignKey(typeof(SellerTable), OnDelete = "CASCADE")] + public long SellerId { get; set; } // Provider + public long PlaceId { get; set; } + 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 99b7c407dc90bc6d5733b8d3ff9c140416613212 Mon Sep 17 00:00:00 2001 From: Nick Evans <2616208+nickevansuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:41:21 +0100 Subject: [PATCH 5/5] feat: Env var to disable token auth (#206) --- Examples/BookingSystem.AspNetCore/Startup.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Examples/BookingSystem.AspNetCore/Startup.cs b/Examples/BookingSystem.AspNetCore/Startup.cs index 4a26db6b..27f63c5d 100644 --- a/Examples/BookingSystem.AspNetCore/Startup.cs +++ b/Examples/BookingSystem.AspNetCore/Startup.cs @@ -18,6 +18,11 @@ public Startup(IConfiguration configuration) { AppSettings = new AppSettings(); configuration.Bind(AppSettings); + + // Provide a simple way to disable token auth for some testing scenarios + if (System.Environment.GetEnvironmentVariable("DISABLE_TOKEN_AUTH") == "true") { + AppSettings.FeatureFlags.EnableTokenAuth = false; + } } public AppSettings AppSettings { get; }