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