From 40bcc70b60e820ca1c630559f357d76ccbc0e393 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 20 Sep 2023 16:31:37 +0100 Subject: [PATCH 01/17] feat: merge Lorem Fitsum features into Reference Implementation feeds --- .../BookingSystem.AspNetCore.csproj | 2 + .../Feeds/FacilitiesFeeds.cs | 358 +++++++++----- .../Feeds/SessionsFeeds.cs | 449 +++++++++++++----- .../Helpers/FeedGenerationHelper.cs | 252 ++++++++++ .../Properties/launchSettings.json | 7 +- Examples/BookingSystem.AspNetCore/README.md | 23 + .../Stores/FacilityStore.cs | 7 +- .../Stores/SessionStore.cs | 4 +- .../FakeBookingSystem.cs | 116 +---- 9 files changed, 858 insertions(+), 360 deletions(-) create mode 100644 Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs diff --git a/Examples/BookingSystem.AspNetCore/BookingSystem.AspNetCore.csproj b/Examples/BookingSystem.AspNetCore/BookingSystem.AspNetCore.csproj index e6d5a34a..62f36fd2 100644 --- a/Examples/BookingSystem.AspNetCore/BookingSystem.AspNetCore.csproj +++ b/Examples/BookingSystem.AspNetCore/BookingSystem.AspNetCore.csproj @@ -3,6 +3,7 @@ netcoreapp3.1 aspnet-BookingSystem.AspNetCore-443B4F82-A20C-41CE-9924-329A0BCF0D14 + Release;Debug @@ -22,4 +23,5 @@ 1701;1702;1591 + diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index e348d1e9..bdf033ba 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -1,4 +1,6 @@ -using OpenActive.DatasetSite.NET; +using Bogus; +using BookingSystem.AspNetCore.Helpers; +using OpenActive.DatasetSite.NET; using OpenActive.FakeDatabase.NET; using OpenActive.NET; using OpenActive.NET.Rpde.Version1; @@ -43,29 +45,134 @@ protected override async Task>> GetRpdeItems(long? af var query = db .SelectMulti(q) - .Select(result => new RpdeItem + .Select(result => { - Kind = RpdeKind.FacilityUse, - Id = result.Item1.Id, - Modified = result.Item1.Modified, - State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, - Data = result.Item1.Deleted ? null : new FacilityUse + var faker = new Faker() { Random = new Randomizer((int)result.Item1.Modified) }; + var isGoldenRecord = faker.Random.Bool(); + + return new RpdeItem { - // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on - // the parent class? Current thinking is it's more extensible on parent class as function signature remains - // constant as power of configuration through underlying class grows (i.e. as new properties are added) - Id = RenderOpportunityId(new FacilityOpportunity + Kind = RpdeKind.FacilityUse, + Id = result.Item1.Id, + Modified = result.Item1.Modified, + State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, + Data = result.Item1.Deleted ? null : new FacilityUse { - OpportunityType = OpportunityType.FacilityUse, // isIndividual?? - FacilityUseId = result.Item1.Id - }), - Name = result.Item1.Name, - Provider = _appSettings.FeatureFlags.SingleSeller ? new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List + // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on + // the parent class? Current thinking is it's more extensible on parent class as function signature remains + // constant as power of configuration through underlying class grows (i.e. as new properties are added) + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.FacilityUse, // isIndividual?? + FacilityUseId = result.Item1.Id + }), + Identifier = result.Item1.Id, + Name = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Name, + Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), + Provider = GenerateOrganizer(result.Item2), + Url = new Uri($"https://www.example.com/facilities/{result.Item1.Id}"), + AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), + AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), + AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)), + IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), + Category = GenerateCategory(faker, isGoldenRecord), + Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord), + Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null, + Location = FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), + FacilityType = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Facility, + 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, + } + }; + }); + + return query.ToList(); + } + } + + private (string Name, List Facility) GetNameAndFacilityTypeForFacility(string databaseTitle, bool isGoldenRecord) + { + // If both FACILITY_TYPE_ID and FACILITY_TYPE_PREF_LABEL env vars are set, these override the randomly generated activity. We also use these to generate an appropriate name + if (Environment.GetEnvironmentVariable("FACILITY_TYPE_ID") != null && Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL") != null) + { + var name = $"{(isGoldenRecord ? "GOLDEN: " : "")} {Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL")} facility"; + var concept = new Concept + { + Id = new Uri(Environment.GetEnvironmentVariable("FACILITY_TYPE_ID")), + PrefLabel = Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL"), + InScheme = new Uri("https://openactive.io/activity-list") + }; + + return (name, new List { concept }); + } + + // If there isn't an override, we use the randomly generated name to derive the appropriate activity + Concept facilityConcept; + switch (databaseTitle) + { + case string a when a.Contains("Sports Hall"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#da364f9b-8bb2-490e-9e2f-1068790b9e35"), + PrefLabel = "Sports Hall", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + case string a when a.Contains("Squash Court"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + case string a when a.Contains("Badminton Court"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#9db5681e-700e-4b30-99a5-355885d94db2"), + PrefLabel = "Badminton Court", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + case string a when a.Contains("Cricket Net"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#2d333183-6a6d-4a95-aad4-c5699f705b14"), + PrefLabel = "Cricket Net", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + default: + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + } + + var nameWithGolden = $"{(isGoldenRecord ? "GOLDEN: " : "")}{databaseTitle}"; + return (nameWithGolden, new List { facilityConcept }); + + } + + private Organization GenerateOrganizer(SellerTable seller) + { + return _appSettings.FeatureFlags.SingleSeller ? new Organization + { + Id = RenderSingleSellerId(), + Name = "Test Seller", + TaxMode = TaxMode.TaxGross, + TermsOfService = new List { new PrivacyPolicy { @@ -74,13 +181,13 @@ protected override async Task>> GetRpdeItems(long? af RequiresExplicitConsent = false } }, - IsOpenBookingAllowed = true, - } : new Organization - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }), - Name = result.Item2.Name, - TaxMode = result.Item2.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List + IsOpenBookingAllowed = true, + } : new Organization + { + Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), + Name = seller.Name, + TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, + TermsOfService = new List { new PrivacyPolicy { @@ -89,33 +196,33 @@ protected override async Task>> GetRpdeItems(long? af RequiresExplicitConsent = false } }, - IsOpenBookingAllowed = true, - }, - Location = _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), - Url = new Uri("https://www.example.com/a-session-age"), - FacilityType = new List { - new Concept - { - Id = new Uri(facilityTypeId), - 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, - } - }); + IsOpenBookingAllowed = true, + }; + } - return query.ToList(); - } + private List GenerateCategory(Faker faker, bool isGoldenRecord) + { + var listOfPossibleCategories = new List + { + "Bookable Facilities", + "Ball Sports", + }; + + return FeedGenerationHelper.GetRandomElementsOf(faker, listOfPossibleCategories, isGoldenRecord, 1).ToList(); + } + + private List GenerateOpeningHours(Faker faker) + { + return new List + { + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Sunday }, Opens = $"{faker.Random.Number(9,12)}:00", Closes = $"{faker.Random.Number(15,17)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Monday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Tuesday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Wednesday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Thursday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Friday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Saturday }, Opens = $"{faker.Random.Number(9,12)}:00", Closes = $"{faker.Random.Number(15,17)}:30"} + }; } } @@ -144,96 +251,91 @@ protected override async Task>> GetRpdeItems(long? afterTime x.Modified == afterTimestamp && x.Id > afterId && x.Modified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) .Take(RpdePageSize) - .Select(x => new RpdeItem + .Select(x => { - Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, - Id = x.Id, - Modified = x.Modified, - State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, - Data = x.Deleted ? null : new Slot + var faker = new Faker() { Random = new Randomizer((int)x.Modified) }; + return new RpdeItem { - // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on - // the parent class? Current thinking is it's more extensible on parent class as function signature remains - // constant as power of configuration through underlying class grows (i.e. as new properties are added) - Id = RenderOpportunityId(new FacilityOpportunity - { - OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, - FacilityUseId = x.FacilityUseId, - SlotId = x.Id, - IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, - }), - FacilityUse = _appSettings.FeatureFlags.FacilityUseHasSlots ? - RenderOpportunityId(new FacilityOpportunity + Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, + Id = x.Id, + Modified = x.Modified, + State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, + Data = x.Deleted ? null : new Slot { - 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, - EndDate = (DateTimeOffset)x.End, - Duration = x.End - x.Start, - RemainingUses = x.RemainingUses - x.LeasedUses, - MaximumUses = x.MaximumUses, - Offers = new List { new Offer - { - Id = RenderOfferId(new FacilityOpportunity - { - OfferId = 0, - OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, - FacilityUseId = x.FacilityUseId, - SlotId = x.Id, - IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, - }), - Price = x.Price, - PriceCurrency = "GBP", - OpenBookingFlowRequirement = OpenBookingFlowRequirement(x), - ValidFromBeforeStartDate = x.ValidFromBeforeStartDate, - LatestCancellationBeforeStartDate = x.LatestCancellationBeforeStartDate, - OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : x.Prepayment.Convert(), - AllowCustomerCancellationFullRefund = x.AllowCustomerCancellationFullRefund, - } - }, - } + // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on + // the parent class? Current thinking is it's more extensible on parent class as function signature remains + // constant as power of configuration through underlying class grows (i.e. as new properties are added) + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, + FacilityUseId = x.FacilityUseId, + SlotId = x.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, + }), + 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, + EndDate = (DateTimeOffset)x.End, + Duration = x.End - x.Start, + RemainingUses = x.RemainingUses - x.LeasedUses, + MaximumUses = x.MaximumUses, + Offers = GenerateOffers(faker, false, x) + } + }; }); return query.ToList(); } } - private static List OpenBookingFlowRequirement(SlotTable slot) + private List GenerateOffers(Faker faker, bool isGoldenRecord, SlotTable slot) { - List openBookingFlowRequirement = null; - - if (slot.RequiresApproval) + var ageRangesForOffers = new List { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingApproval); - } + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult"}, + new QuantitativeValue { MaxValue = 17, Name = "Junior"}, + new QuantitativeValue {MinValue = 60, Name = "Senior"}, + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult (off-peak)"}, + }; - if (slot.RequiresAttendeeValidation) + Offer GenerateOffer(SlotTable slot, QuantitativeValue ageRange) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingAttendeeDetails); + return new Offer + { + Id = RenderOfferId(new FacilityOpportunity + { + OfferId = 0, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, + FacilityUseId = slot.FacilityUseId, + SlotId = slot.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? slot.IndividualFacilityUseId : null, + }), + Price = slot.Price, + PriceCurrency = "GBP", + OpenBookingFlowRequirement = FeedGenerationHelper.OpenBookingFlowRequirement(slot.RequiresApproval, slot.RequiresAttendeeValidation, slot.RequiresAdditionalDetails, slot.AllowsProposalAmendment), + ValidFromBeforeStartDate = slot.ValidFromBeforeStartDate, + LatestCancellationBeforeStartDate = slot.LatestCancellationBeforeStartDate, + OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : slot.Prepayment.Convert(), + AllowCustomerCancellationFullRefund = slot.AllowCustomerCancellationFullRefund, + AcceptedPaymentMethod = new List { PaymentMethod.Cash, PaymentMethod.PaymentMethodCreditCard }, + }; } - if (slot.RequiresAdditionalDetails) - { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingIntakeForm); - } + var allOffersForAllAgeRanges = ageRangesForOffers.Select(ageRange => GenerateOffer(slot, ageRange)).ToList(); - if (slot.AllowsProposalAmendment) - { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingNegotiation); - } - return openBookingFlowRequirement; + return FeedGenerationHelper.GetRandomElementsOf(faker, allOffersForAllAgeRanges, isGoldenRecord, 1, 4).ToList(); } + } } \ No newline at end of file diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index b803b921..e839dd95 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -8,6 +8,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bogus; +using BookingSystem.AspNetCore.Helpers; +using ServiceStack; +using System.Globalization; namespace BookingSystem { @@ -85,9 +89,6 @@ 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() @@ -102,140 +103,370 @@ protected override async Task>> GetRpdeItems(long? var query = db .SelectMulti(q) - .Select(result => new RpdeItem + // here we randomly decide whether the item is going to be a golden record or not by using Faker + // See the README for more detail on golden records. + .Select(result => { - Kind = RpdeKind.SessionSeries, - Id = result.Item1.Id, - Modified = result.Item1.Modified, - State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, - Data = result.Item1.Deleted ? null : new SessionSeries + var faker = new Faker() { Random = new Randomizer((int)result.Item1.Modified) }; + var isGoldenRecord = faker.Random.Bool(); + + return new RpdeItem { - // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on - // the parent class? Current thinking is it's more extensible on parent class as function signature remains - // constant as power of configuration through underlying class grows (i.e. as new properties are added) - Id = RenderOpportunityId(new SessionOpportunity + Kind = RpdeKind.SessionSeries, + Id = result.Item1.Id, + Modified = result.Item1.Modified, + State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, + Data = result.Item1.Deleted ? null : new SessionSeries { - OpportunityType = OpportunityType.SessionSeries, - SessionSeriesId = result.Item1.Id - }), - Name = result.Item1.Title, - EventAttendanceMode = MapAttendanceMode(result.Item1.AttendanceMode), - Organizer = _appSettings.FeatureFlags.SingleSeller ? new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - } : result.Item2.IsIndividual ? (ILegalEntity)new Person - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }), - Name = result.Item2.Name, - TaxMode = result.Item2.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - IsOpenBookingAllowed = true, - } : (ILegalEntity)new Organization - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }), - Name = result.Item2.Name, - TaxMode = result.Item2.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - }, - Offers = new List { new Offer + // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on + // the parent class? Current thinking is it's more extensible on parent class as function signature remains + // constant as power of configuration through underlying class grows (i.e. as new properties are added) + Id = RenderOpportunityId(new SessionOpportunity { - Id = RenderOfferId(new SessionOpportunity - { - OfferOpportunityType = OpportunityType.SessionSeries, - SessionSeriesId = result.Item1.Id, - OfferId = 0 - }), - Price = result.Item1.Price, - PriceCurrency = "GBP", - OpenBookingFlowRequirement = OpenBookingFlowRequirement(result.Item1), - ValidFromBeforeStartDate = result.Item1.ValidFromBeforeStartDate, - LatestCancellationBeforeStartDate = result.Item1.LatestCancellationBeforeStartDate, - OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : result.Item1.Prepayment.Convert(), - AllowCustomerCancellationFullRefund = result.Item1.AllowCustomerCancellationFullRefund - } - }, - 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 - { - new Concept - { - Id = new Uri(activityId), - PrefLabel = activityPrefLabel, - InScheme = new Uri("https://openactive.io/activity-list") - } + OpportunityType = OpportunityType.SessionSeries, + SessionSeriesId = result.Item1.Id + }), + Identifier = result.Item1.Id, + Name = GetNameAndActivityForSessions(result.Item1.Title, isGoldenRecord).Name, + EventAttendanceMode = MapAttendanceMode(result.Item1.AttendanceMode), + Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), + AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), + GenderRestriction = faker.Random.Enum(), + AgeRange = GenerateAgeRange(faker, isGoldenRecord), + Level = faker.Random.ListItems(new List { "Beginner", "Intermediate", "Advanced" }, 1).ToList(), + Organizer = GenerateOrganizerOrPerson(result.Item2), + AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), + AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)), + IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), + Category = GenerateCategory(faker, isGoldenRecord), + Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord), + Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null, + Leader = GenerateListOfPersons(faker, isGoldenRecord, 2), + Contributor = GenerateListOfPersons(faker, isGoldenRecord, 2), + IsCoached = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), + Offers = GenerateOffers(faker, isGoldenRecord, result.Item1), + // location MUST not be provided for fully virtual sessions + Location = result.Item1.AttendanceMode == AttendanceMode.Online ? null : FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), + // beta:affiliatedLocation MAY be provided for fully virtual sessions + AffiliatedLocation = (result.Item1.AttendanceMode == AttendanceMode.Offline && faker.Random.Bool()) ? null : FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), + EventSchedule = GenerateSchedules(faker, isGoldenRecord), + SchedulingNote = GenerateSchedulingNote(faker, isGoldenRecord), + IsAccessibleForFree = result.Item1.Price == 0, + Url = new Uri($"https://www.example.com/sessions/{result.Item1.Id}"), + Activity = GetNameAndActivityForSessions(result.Item1.Title, isGoldenRecord).Activity, + Programme = GenerateBrand(faker, isGoldenRecord), + IsInteractivityPreferred = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })), + IsVirtuallyCoached = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })), + ParticipantSuppliedEquipment = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? OpenActive.NET.RequiredStatusType.Optional : faker.Random.ListItem(new List { OpenActive.NET.RequiredStatusType.Optional, OpenActive.NET.RequiredStatusType.Required, OpenActive.NET.RequiredStatusType.Unavailable, null })), } - } - }); ; + }; + }); return query.ToList(); } } + // differences between refimpl and lorem: location generation is one of 3 hardcoded not randomly genned, all schedules are partial, ss doesnt contain scs data like maxAttendeeCapacity, + + private static EventAttendanceModeEnumeration MapAttendanceMode(AttendanceMode attendanceMode) + { + switch (attendanceMode) + { + case AttendanceMode.Offline: + return EventAttendanceModeEnumeration.OfflineEventAttendanceMode; + case AttendanceMode.Online: + return EventAttendanceModeEnumeration.OnlineEventAttendanceMode; + case AttendanceMode.Mixed: + return EventAttendanceModeEnumeration.MixedEventAttendanceMode; + default: + throw new OpenBookingException(new OpenBookingError(), $"AttendanceMode Type {attendanceMode} not supported"); + } + } + + private (string Name, List Activity) GetNameAndActivityForSessions(string databaseTitle, bool isGoldenRecord) + { + // If both ACTIVITY_ID and ACTIVITY_PREF_LABEL env vars are set, these override the randomly generated activity. We also use these to generate an appropriate name + if (Environment.GetEnvironmentVariable("ACTIVITY_ID") != null && Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL") != null) + { + var name = $"{(isGoldenRecord ? "GOLDEN: " : "")} {Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL")} class"; + var concept = new Concept + { + Id = new Uri(Environment.GetEnvironmentVariable("ACTIVITY_ID")), + PrefLabel = Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL"), + InScheme = new Uri("https://openactive.io/activity-list") + }; + + return (name, new List { concept }); + } + + // If there isn't an override, we use the randomly generated name to derive the appropriate activity + Concept activityConcept; + switch (databaseTitle) + { + case string a when a.Contains("Yoga"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#bf1a5e00-cdcf-465d-8c5a-6f57040b7f7e"), + PrefLabel = "Yoga", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Zumba"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#78503fa2-ed24-4a80-a224-e2e94581d8a8"), + PrefLabel = "Zumba®", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Walking"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#95092977-5a20-4d6e-b312-8fddabe71544"), + PrefLabel = "Walking", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Cycling"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#4a19873e-118e-43f4-b86e-05acba8fb1de"), + PrefLabel = "Cycling", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Running"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#72ddb2dc-7d75-424e-880a-d90eabe91381"), + PrefLabel = "Running", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Jumping"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#8a4abff3-c616-4f33-80a1-398b88c672a3"), + PrefLabel = "World Jumping®", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + default: + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#c07d63a0-8eb9-4602-8bcc-23be6deb8f83"), + PrefLabel = "Jet Skiing", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + } + + var nameWithGolden = $"{(isGoldenRecord ? "GOLDEN: " : "")}{databaseTitle}"; + return (nameWithGolden, new List { activityConcept }); + + } + + private QuantitativeValue GenerateAgeRange(Faker faker, bool isGoldenRecord) + { + var ageRange = new QuantitativeValue(); + if (isGoldenRecord || faker.Random.Bool()) ageRange.MaxValue = faker.Random.Number(80); + if (isGoldenRecord || faker.Random.Bool()) ageRange.MinValue = faker.Random.Number(60); + + if (ageRange.MaxValue == null && ageRange.MinValue == null) ageRange.MinValue = 0; + return ageRange; + } - private static List OpenBookingFlowRequirement(ClassTable @class) + private ILegalEntity GenerateOrganizerOrPerson(SellerTable seller) { - List openBookingFlowRequirement = null; + if (_appSettings.FeatureFlags.SingleSeller) + return new Organization + { + Id = RenderSingleSellerId(), + Name = "Test Seller", + TaxMode = TaxMode.TaxGross, + TermsOfService = new List + { + new PrivacyPolicy + { + Name = "Privacy Policy", + Url = new Uri("https://example.com/privacy.html"), + RequiresExplicitConsent = false + } + }, + IsOpenBookingAllowed = true, + }; + if (seller.IsIndividual) + return new OpenActive.NET.Person + { + Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), + Name = seller.Name, + TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, + IsOpenBookingAllowed = true, + }; + return new Organization + { + Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), + Name = seller.Name, + TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, + TermsOfService = new List + { + new PrivacyPolicy + { + Name = "Privacy Policy", + Url = new Uri("https://example.com/privacy.html"), + RequiresExplicitConsent = false + } + }, + IsOpenBookingAllowed = true, + }; + } - if (@class.RequiresApproval) + private List GenerateCategory(Faker faker, bool isGoldenRecord) + { + var listOfPossibleCategories = new List { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingApproval); + "Group Exercise Classes", + "Toning & Strength", + "Group Exercise - Virtual" + }; + + return FeedGenerationHelper.GetRandomElementsOf(faker, listOfPossibleCategories, isGoldenRecord, 1).ToList(); + } + + private List GenerateListOfPersons(Faker faker, bool isGoldenRecord, int possibleMax) + { + static OpenActive.NET.Person GeneratePerson(Faker faker, bool isGoldenRecord) + { + var id = faker.Finance.Bic(); + var genderIndex = faker.Random.Number(1); + var gender = (Bogus.DataSets.Name.Gender)genderIndex; + var givenName = faker.Name.FirstName(gender); + var familyName = faker.Name.LastName(gender); + var name = $"{givenName} {familyName}"; + var isLiteRecord = isGoldenRecord ? false : faker.Random.Bool(); + + return new OpenActive.NET.Person + { + Id = new Uri($"https://example.com/people/{id}"), + Identifier = id, + Name = name, + GivenName = isLiteRecord ? null : givenName, + FamilyName = isLiteRecord ? null : familyName, + Gender = genderIndex == 1 ? Schema.NET.GenderType.Female : Schema.NET.GenderType.Male, + JobTitle = faker.Random.ListItem(new List { "Leader", "Team leader", "Host", "Instructor", "Coach" }), + Telephone = isLiteRecord ? null : faker.Phone.PhoneNumber("07## ### ####"), + Email = isLiteRecord ? null : faker.Internet.ExampleEmail(), + Url = new Uri($"{faker.Internet.Url()}/profile/{faker.Random.Number(50)}"), + Image = new Schema.NET.ImageObject { Url = new Uri(faker.Internet.Avatar()) } + }; } - if (@class.RequiresAttendeeValidation) + var output = new List(); + var max = isGoldenRecord ? possibleMax : faker.Random.Number(possibleMax); + for (var i = 0; i < max; i++) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingAttendeeDetails); + output.Add(GeneratePerson(faker, isGoldenRecord)); } + return output; + } - if (@class.RequiresAdditionalDetails) + private List GenerateSchedules(Faker faker, bool isGoldenRecord) + { + var schedules = new List(); + PartialSchedule GenerateSchedule(Faker faker) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingIntakeForm); + var startTimeString = $"{faker.Random.Number(min: 10, max: 22)}:{faker.Random.ListItem(new List { "00", "15", "30", "45" })}:00"; + var startTime = new TimeValue(startTimeString); + var duration = faker.Random.ListItem(new List { new TimeSpan(0, 30, 0), new TimeSpan(1, 0, 0), new TimeSpan(1, 30, 0), new TimeSpan(2, 0, 0) }); + var startTimeSpan = TimeSpan.Parse(startTimeString); + + var endTime = new DateTime(startTimeSpan.Add(duration).Ticks); + var endTimeString = endTime.ToString("HH:mm"); + var endTimeTM = new TimeValue(endTimeString); + var startDateFaker = faker.Date.Soon(); + var startDate = new DateValue(startDateFaker); + var endDate = new DateValue(faker.Date.Soon(28, startDateFaker)); + + var partialSchedule = new PartialSchedule + { + StartTime = startTime, + Duration = duration, + EndTime = endTimeTM, + StartDate = startDate, + EndDate = endDate, + RepeatFrequency = faker.Random.ListItem(new List { new TimeSpan(7, 0, 0, 0), new TimeSpan(14, 0, 0, 0) }), + ByDay = faker.Random.EnumValues().ToList() + }; + return partialSchedule; } - if (@class.AllowsProposalAmendment) + for (var i = 0; i < 2; i++) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingNegotiation); + schedules.Add(GenerateSchedule(faker)); } - return openBookingFlowRequirement; + + return FeedGenerationHelper.GetRandomElementsOf(faker, schedules, isGoldenRecord, 0, 1).ToList(); } - private static EventAttendanceModeEnumeration MapAttendanceMode(AttendanceMode attendanceMode) + private string GenerateSchedulingNote(Faker faker, bool isGoldenRecord) { - switch (attendanceMode) + var allSchedulingNotes = new List { - case AttendanceMode.Offline: - return EventAttendanceModeEnumeration.OfflineEventAttendanceMode; - case AttendanceMode.Online: - return EventAttendanceModeEnumeration.OnlineEventAttendanceMode; - case AttendanceMode.Mixed: - return EventAttendanceModeEnumeration.MixedEventAttendanceMode; - default: - throw new OpenBookingException(new OpenBookingError(), $"AttendanceMode Type {attendanceMode} not supported"); + "Sessions are not running during school holidays.", + "Sessions may be cancelled with 15 minutes notice, please keep an eye on your e-mail.", + "Sessions are scheduled with best intentions, but sometimes need to be rescheduled due to venue availability. Ensure that you contact the organizer before turning up." + }; + + if (isGoldenRecord) return faker.Random.ListItem(allSchedulingNotes); + return faker.Random.Bool() ? faker.Random.ListItem(allSchedulingNotes) : null; + } + + private List GenerateOffers(Faker faker, bool isGoldenRecord, ClassTable @class) + { + var ageRangesForOffers = new List + { + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult"}, + new QuantitativeValue { MaxValue = 17, Name = "Junior"}, + new QuantitativeValue {MinValue = 60, Name = "Senior"}, + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult (off-peak)"}, + }; + + Offer GenerateOffer(ClassTable @class, QuantitativeValue ageRange) + { + return new Offer + { + Id = RenderOfferId(new SessionOpportunity + { + OfferOpportunityType = OpportunityType.SessionSeries, + SessionSeriesId = @class.Id, + OfferId = 0 + }), + Price = @class.Price, + PriceCurrency = "GBP", + OpenBookingFlowRequirement = FeedGenerationHelper.OpenBookingFlowRequirement(@class.RequiresApproval, @class.RequiresAttendeeValidation, @class.RequiresAdditionalDetails, @class.AllowsProposalAmendment), + ValidFromBeforeStartDate = @class.ValidFromBeforeStartDate, + LatestCancellationBeforeStartDate = @class.LatestCancellationBeforeStartDate, + OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : @class.Prepayment.Convert(), + AllowCustomerCancellationFullRefund = @class.AllowCustomerCancellationFullRefund, + AcceptedPaymentMethod = new List { PaymentMethod.Cash, PaymentMethod.PaymentMethodCreditCard }, + AgeRestriction = ageRange, + }; } + + var allOffersForAllAgeRanges = ageRangesForOffers.Select(ageRange => GenerateOffer(@class, ageRange)).ToList(); + + return FeedGenerationHelper.GetRandomElementsOf(faker, allOffersForAllAgeRanges, isGoldenRecord, 1, 2).ToList(); + } + + private Brand GenerateBrand(Faker faker, bool isGoldenRecord) + { + return new Brand + { + Name = faker.Random.ListItem(new List { "Keyways Active", "This Girl Can", "Back to Activity", "Mega-active Super Dads" }), + Url = new Uri(faker.Internet.Url()), + Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), + Logo = new ImageObject { Url = new Uri(faker.Internet.Avatar()) }, + Video = new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=N268gBOvnzo") } } + }; } } } diff --git a/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs b/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs new file mode 100644 index 00000000..fb36a4f5 --- /dev/null +++ b/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs @@ -0,0 +1,252 @@ +using System; +using OpenActive.NET; +using System.Collections.Generic; +using OpenActive.FakeDatabase.NET; +using Bogus; +using System.Linq; + +namespace BookingSystem.AspNetCore.Helpers +{ + public static class FeedGenerationHelper + { + public static IList GetRandomElementsOf(Faker faker, IList list, bool isGoldenRecord, int minimumNumberOfElements = 0, int maximumNumberOfElements = 0) + { + // If this is for the golden record, return the whole list so that all the possible data values are returned + if (isGoldenRecord) return list; + + // If maximumNumberOfElements is the default value, use list.Count, if it's been set, use that + var max = maximumNumberOfElements == 0 ? list.Count : maximumNumberOfElements; + // Otherwise return a random number of elements from the list + var randomNumberOfElementsToReturn = faker.Random.Number(minimumNumberOfElements, max); + return faker.Random.ListItems(list, randomNumberOfElementsToReturn); + } + + + public static Place GetPlaceById(long placeId) + { + // Three hardcoded fake places + switch (placeId) + { + case 1: + return new 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 PostalAddress + { + StreetAddress = "Kings Mead House", + AddressLocality = "Oxford", + AddressRegion = "Oxfordshire", + PostalCode = "OX1 1AA", + AddressCountry = "GB" + }, + Geo = new GeoCoordinates + { + Latitude = (decimal?)51.7502, + Longitude = (decimal?)-1.2674 + }, + Image = new List { + new 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 ChangingFacilities { Name = "Changing Facilities", Value = true }, + new Showers { Name = "Showers", Value = true }, + new Lockers { Name = "Lockers", Value = true }, + new Towels { Name = "Towels", Value = false }, + new Creche { Name = "Creche", Value = false }, + new Parking { Name = "Parking", Value = false } + }, + OpeningHoursSpecification = new List + { + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Sunday }, Opens = "09:00", Closes = "17:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Monday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Tuesday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Wednesday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Thursday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Friday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Saturday }, Opens = "09:00", Closes = "17:30"} + } + }; + case 2: + return new 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 PostalAddress + { + StreetAddress = "Greyfriars Court, Paradise Square", + AddressLocality = "Oxford", + AddressRegion = "Oxfordshire", + PostalCode = "OX1 1BB", + AddressCountry = "GB" + }, + Geo = new GeoCoordinates + { + Latitude = (decimal?)51.7504933, + Longitude = (decimal?)-1.2620685 + }, + Image = new List { + new 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 ChangingFacilities { Name = "Changing Facilities", Value = false }, + new Showers { Name = "Showers", Value = false }, + new Lockers { Name = "Lockers", Value = false }, + new Towels { Name = "Towels", Value = true }, + new Creche { Name = "Creche", Value = true }, + new Parking { Name = "Parking", Value = true } + }, + OpeningHoursSpecification = new List + { + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Sunday }, Opens = "09:00", Closes = "17:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Monday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Tuesday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Wednesday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Thursday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Friday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Saturday }, Opens = "09:00", Closes = "17:30"} + } + }; + case 3: + return new 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 PostalAddress + { + StreetAddress = "Norfolk Street", + AddressLocality = "Oxford", + AddressRegion = "Oxfordshire", + PostalCode = "OX1 1UU", + AddressCountry = "GB" + }, + Geo = new GeoCoordinates + { + Latitude = (decimal?)51.749826, + Longitude = (decimal?)-1.261492 + }, + Image = new List { + new 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"), + OpeningHoursSpecification = new List + { + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Sunday }, Opens = "09:00", Closes = "17:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Monday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Tuesday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Wednesday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Thursday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Friday }, Opens = "06:30", Closes = "21:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Saturday }, Opens = "09:00", Closes = "17:30"} + } + }; + default: + return null; + } + } + + public static List OpenBookingFlowRequirement(bool requiresApproval, bool requiresAttendeeValidation, bool requiresAdditionalDetails, bool allowsProposalAmendment) + { + List openBookingFlowRequirement = null; + + if (requiresApproval) + { + openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); + openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingApproval); + } + + if (requiresAttendeeValidation) + { + openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); + openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingAttendeeDetails); + } + + if (requiresAdditionalDetails) + { + openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); + openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingIntakeForm); + } + + if (allowsProposalAmendment) + { + openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); + openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingNegotiation); + } + return openBookingFlowRequirement; + } + + public static string GenerateAttendeeInstructions(Faker faker, bool isGoldenRecord) + { + var listOfPossibleInstructions = new List(){ + "wear sportswear/gym clothes", + "wear comfortable loose clothing", + "come as you are", + "bring trainers", + "wear flat shoes", + "no footwear required" + }; + + return $"Clothing instructions: {string.Join(", ", GetRandomElementsOf(faker, listOfPossibleInstructions, isGoldenRecord, 1))}"; + } + + public static List GenerateAccessibilitySupport(Faker faker, bool isGoldenRecord) + { + var listOfAccessibilitySupports = new List + { + new Concept {Id = new Uri("https://openactive.io/accessibility-support#1393f2dc-3fcc-4be9-a99f-f1e51f5ad277"), PrefLabel = "Visual Impairment"}, + new Concept {Id = new Uri("https://openactive.io/accessibility-support#2bfb7228-5969-4927-8435-38b5005a8771"), PrefLabel = "Hearing Impairment"}, + new Concept {Id = new Uri("https://openactive.io/accessibility-support#40b9b11f-bdd3-4aeb-8984-2ecf74a14c7a"), PrefLabel = "Mental health issues"} + }; + + return GetRandomElementsOf(faker, listOfAccessibilitySupports, isGoldenRecord, 1, 2).ToList(); + } + + public static List GenerateImages(Faker faker, bool isGoldenRecord) + { + static Uri GenerateImageUrl(int width, int height, int seed) + { + return new Uri($"https://picsum.photos/{width}/{height}?image={seed}"); + } + + var images = new List(); + var min = isGoldenRecord ? 4 : 0; + var imageCount = faker.Random.Number(min, 3); + for (var i = 0; i < imageCount; i++) + { + var imageSeed = faker.Random.Number(1083); + var thumbnails = new List { + new ImageObject{Url = GenerateImageUrl(672, 414, imageSeed), Width = 672, Height = 414}, + new ImageObject{Url = GenerateImageUrl(300, 200, imageSeed), Width = 300, Height = 200}, + new ImageObject{Url = GenerateImageUrl(100, 100, imageSeed), Width = 100, Height = 100} + }; + var image = new ImageObject + { + Url = GenerateImageUrl(1024, 724, imageSeed), + Thumbnail = GetRandomElementsOf(faker, thumbnails, isGoldenRecord, 0, 1).ToList() + }; + images.Add(image); + } + return images; + } + } +} + diff --git a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json index 30b0586b..9c926cbb 100644 --- a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json +++ b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json @@ -6,12 +6,11 @@ "profiles": { "BookingSystem.AspNetCore": { "commandName": "Project", - "launchBrowser": true, - "launchUrl": "https://localhost:5001/openactive", - "applicationUrl": "https://localhost:5001", + "launchUrl": "https://localhost:5002/openactive", + "applicationUrl": "https://localhost:5003", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "ApplicationHostBaseUrl": "https://localhost:5001" + "ApplicationHostBaseUrl": "https://localhost:5003" } } } diff --git a/Examples/BookingSystem.AspNetCore/README.md b/Examples/BookingSystem.AspNetCore/README.md index 6b84a93f..6ddf6fba 100644 --- a/Examples/BookingSystem.AspNetCore/README.md +++ b/Examples/BookingSystem.AspNetCore/README.md @@ -15,3 +15,26 @@ This implementation is also used as a reference implementation for the [Test Sui `ApplicationHostBaseUrl: http://localhost:{PORT}` 3. Now, re-run the project. You're good to go 👍 + + +## Reference Implementation Data Generation + +Reference Implementation has three main uses that make it very important in the OpenActive ecosystem: +- For data publishers / booking systems: It is used to demonstrate the properties and shape of data and APIs, according to the OpenActive specifications +- For data users / brokers: It is used as a trial integration where testing can be done with no ramifications +- For contributors: It is used to ensure the Test Suite tests are correct and passing, for different combinations of Open Booking API features. + +The data for the sample feeds are generated in two places: +- BookingSystem.AspNetCore/Feeds/*Feeds.cs +- OpenActive.FakeDatabase.NET/Fakes/FakeBookingSystem.cs + +The FakeBookingSystem within OpenActive.FakeDatabase.NET acts as the interface to an example database. +The example Feeds within BookingSystem.AspNetCore query this interface and translate the data to conform with the OpenActive Modelling Spec. + +Due to this split of functionality, the sample data in the feeds are created/transformed in both files, depending on whether they are important to booking +or not. For example, `Price` is important to booking and there is generated in FakeBookingSystem at startup and stored in the in-memory database. However `Terms Of Service` is not +needed for booking, and therefore is generated at request time. + +### Golden Records +Golden records are randomly generated records that contain all possible fields specified by the OpenActive Modelling Specification. +They are unrealistic representations of data, and the presence of all the fields should not be relied on when developing front-end representations of the data. \ No newline at end of file diff --git a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs index 8737ba2e..a8381c34 100644 --- a/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetCore/Stores/FacilityStore.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BookingSystem.AspNetCore.Helpers; using OpenActive.DatasetSite.NET; using OpenActive.FakeDatabase.NET; using OpenActive.NET; @@ -320,7 +321,7 @@ protected override async Task GetOrderItems(List { new Concept { @@ -374,11 +375,11 @@ protected override async Task GetOrderItems(List { new Concept diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index f30ab2e9..ed8e3bbd 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -644,118 +644,6 @@ 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()) @@ -1497,13 +1385,13 @@ private static async Task CreateFakeFacilitiesAndSlots(IDbConnection db, bool fa 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 facilityUseName = $"{Faker.Commerce.ProductMaterial()} {Faker.PickRandomParam("Sports Hall", "Squash Court", "Badminton Court", "Cricket Net")}"; var facility = new FacilityUseTable { Id = seed.Id, Deleted = false, Name = facilityUseName, - SellerId = Faker.Random.Bool(0.8f) ? Faker.Random.Long(1, 2) : Faker.Random.Long(3, 5), // distribution: 80% 1-2, 20% 3-5 + SellerId = Faker.Random.Bool(0.8f) ? Faker.Random.Long(1, 2) : Faker.Random.Long(3, 4), // distribution: 80% 1-2, 20% 3-5 PlaceId = Faker.PickRandom(new[] { 1, 2, 3 }) }; From fc0ed3324eae651bc2979ec35cd2ae7c5128ad55 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 20 Sep 2023 16:41:52 +0100 Subject: [PATCH 02/17] remove comment and fix port change --- Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs | 6 ++---- .../BookingSystem.AspNetCore/Properties/launchSettings.json | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index e839dd95..ebb06a7d 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -103,11 +103,11 @@ protected override async Task>> GetRpdeItems(long? var query = db .SelectMulti(q) - // here we randomly decide whether the item is going to be a golden record or not by using Faker - // See the README for more detail on golden records. .Select(result => { var faker = new Faker() { Random = new Randomizer((int)result.Item1.Modified) }; + // here we randomly decide whether the item is going to be a golden record or not by using Faker + // See the README for more detail on golden records. var isGoldenRecord = faker.Random.Bool(); return new RpdeItem @@ -165,8 +165,6 @@ protected override async Task>> GetRpdeItems(long? return query.ToList(); } } - // differences between refimpl and lorem: location generation is one of 3 hardcoded not randomly genned, all schedules are partial, ss doesnt contain scs data like maxAttendeeCapacity, - private static EventAttendanceModeEnumeration MapAttendanceMode(AttendanceMode attendanceMode) { switch (attendanceMode) diff --git a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json index 9c926cbb..bcd3c51f 100644 --- a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json +++ b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json @@ -7,10 +7,10 @@ "BookingSystem.AspNetCore": { "commandName": "Project", "launchUrl": "https://localhost:5002/openactive", - "applicationUrl": "https://localhost:5003", + "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "ApplicationHostBaseUrl": "https://localhost:5003" + "ApplicationHostBaseUrl": "https://localhost:5001" } } } From bcaab68935985bfd45333e94ddd7acfc024b4280 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 20 Sep 2023 16:52:43 +0100 Subject: [PATCH 03/17] fix seed logic --- Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs | 4 ++-- Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index bdf033ba..69cb9c4b 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -47,7 +47,7 @@ protected override async Task>> GetRpdeItems(long? af .SelectMulti(q) .Select(result => { - var faker = new Faker() { Random = new Randomizer((int)result.Item1.Modified) }; + var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; var isGoldenRecord = faker.Random.Bool(); return new RpdeItem @@ -253,7 +253,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => { - var faker = new Faker() { Random = new Randomizer((int)x.Modified) }; + var faker = new Faker() { Random = new Randomizer(((int)x.Modified + (int)x.Id)) }; return new RpdeItem { Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index ebb06a7d..2cc03799 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -105,7 +105,9 @@ protected override async Task>> GetRpdeItems(long? .SelectMulti(q) .Select(result => { - var faker = new Faker() { Random = new Randomizer((int)result.Item1.Modified) }; + var intt = (int)result.Item1.Modified; + + var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; // here we randomly decide whether the item is going to be a golden record or not by using Faker // See the README for more detail on golden records. var isGoldenRecord = faker.Random.Bool(); From 97440420e2ddc4b5cdd796a992bd25af40de8630 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 21 Sep 2023 16:25:06 +0100 Subject: [PATCH 04/17] feed validator checks --- .../Feeds/SessionsFeeds.cs | 17 ++++++++++++----- .../Helpers/FeedGenerationHelper.cs | 11 +++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index 2cc03799..7f19a996 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -136,7 +136,7 @@ protected override async Task>> GetRpdeItems(long? GenderRestriction = faker.Random.Enum(), AgeRange = GenerateAgeRange(faker, isGoldenRecord), Level = faker.Random.ListItems(new List { "Beginner", "Intermediate", "Advanced" }, 1).ToList(), - Organizer = GenerateOrganizerOrPerson(result.Item2), + Organizer = GenerateOrganizerOrPerson(faker, result.Item2), AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)), IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), @@ -268,14 +268,14 @@ private static EventAttendanceModeEnumeration MapAttendanceMode(AttendanceMode a private QuantitativeValue GenerateAgeRange(Faker faker, bool isGoldenRecord) { var ageRange = new QuantitativeValue(); - if (isGoldenRecord || faker.Random.Bool()) ageRange.MaxValue = faker.Random.Number(80); - if (isGoldenRecord || faker.Random.Bool()) ageRange.MinValue = faker.Random.Number(60); + if (isGoldenRecord || faker.Random.Bool()) ageRange.MaxValue = faker.Random.Number(16, 100); + if (isGoldenRecord || faker.Random.Bool()) ageRange.MinValue = faker.Random.Number(0, ageRange.MaxValue == null ? (int)ageRange.MaxValue : 100); if (ageRange.MaxValue == null && ageRange.MinValue == null) ageRange.MinValue = 0; return ageRange; } - private ILegalEntity GenerateOrganizerOrPerson(SellerTable seller) + private ILegalEntity GenerateOrganizerOrPerson(Faker faker, SellerTable seller) { if (_appSettings.FeatureFlags.SingleSeller) return new Organization @@ -293,6 +293,8 @@ private ILegalEntity GenerateOrganizerOrPerson(SellerTable seller) } }, IsOpenBookingAllowed = true, + Telephone = faker.Phone.PhoneNumber("0#### ######"), + SameAs = new List { new Uri("https://socialmedia/testseller") } }; if (seller.IsIndividual) return new OpenActive.NET.Person @@ -301,6 +303,7 @@ private ILegalEntity GenerateOrganizerOrPerson(SellerTable seller) Name = seller.Name, TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, IsOpenBookingAllowed = true, + Telephone = faker.Phone.PhoneNumber("07### ######") }; return new Organization { @@ -317,6 +320,9 @@ private ILegalEntity GenerateOrganizerOrPerson(SellerTable seller) } }, IsOpenBookingAllowed = true, + Url = new Uri(faker.Internet.Url()), + Telephone = faker.Phone.PhoneNumber("0#### ######"), + SameAs = new List { new Uri($"https://socialmedia/{seller.Name}") } }; } @@ -404,7 +410,7 @@ PartialSchedule GenerateSchedule(Faker faker) schedules.Add(GenerateSchedule(faker)); } - return FeedGenerationHelper.GetRandomElementsOf(faker, schedules, isGoldenRecord, 0, 1).ToList(); + return FeedGenerationHelper.GetRandomElementsOf(faker, schedules, isGoldenRecord, 1, 1).ToList(); } private string GenerateSchedulingNote(Faker faker, bool isGoldenRecord) @@ -442,6 +448,7 @@ Offer GenerateOffer(ClassTable @class, QuantitativeValue ageRange) }), Price = @class.Price, PriceCurrency = "GBP", + Name = ageRange.Name, OpenBookingFlowRequirement = FeedGenerationHelper.OpenBookingFlowRequirement(@class.RequiresApproval, @class.RequiresAttendeeValidation, @class.RequiresAdditionalDetails, @class.AllowsProposalAmendment), ValidFromBeforeStartDate = @class.ValidFromBeforeStartDate, LatestCancellationBeforeStartDate = @class.LatestCancellationBeforeStartDate, diff --git a/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs b/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs index fb36a4f5..f65e956d 100644 --- a/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs +++ b/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs @@ -31,6 +31,7 @@ public static Place GetPlaceById(long placeId) return new Place { Identifier = 1, + Id = new Uri($"https://example.com/place/{placeId}"), 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 PostalAddress @@ -78,6 +79,7 @@ public static Place GetPlaceById(long placeId) return new Place { Identifier = 2, + Id = new Uri($"https://example.com/place/{placeId}"), 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 PostalAddress @@ -125,6 +127,7 @@ public static Place GetPlaceById(long placeId) return new Place { Identifier = 3, + Id = new Uri($"https://example.com/place/{placeId}"), 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 PostalAddress @@ -212,9 +215,9 @@ public static List GenerateAccessibilitySupport(Faker faker, bool isGol { var listOfAccessibilitySupports = new List { - new Concept {Id = new Uri("https://openactive.io/accessibility-support#1393f2dc-3fcc-4be9-a99f-f1e51f5ad277"), PrefLabel = "Visual Impairment"}, - new Concept {Id = new Uri("https://openactive.io/accessibility-support#2bfb7228-5969-4927-8435-38b5005a8771"), PrefLabel = "Hearing Impairment"}, - new Concept {Id = new Uri("https://openactive.io/accessibility-support#40b9b11f-bdd3-4aeb-8984-2ecf74a14c7a"), PrefLabel = "Mental health issues"} + new Concept {Id = new Uri("https://openactive.io/accessibility-support#1393f2dc-3fcc-4be9-a99f-f1e51f5ad277"), PrefLabel = "Visual Impairment", InScheme = new Uri("https://openactive.io/accessibility-support")}, + new Concept {Id = new Uri("https://openactive.io/accessibility-support#2bfb7228-5969-4927-8435-38b5005a8771"), PrefLabel = "Hearing Impairment", InScheme = new Uri("https://openactive.io/accessibility-support")}, + new Concept {Id = new Uri("https://openactive.io/accessibility-support#40b9b11f-bdd3-4aeb-8984-2ecf74a14c7a"), PrefLabel = "Mental health issues", InScheme = new Uri("https://openactive.io/accessibility-support")} }; return GetRandomElementsOf(faker, listOfAccessibilitySupports, isGoldenRecord, 1, 2).ToList(); @@ -241,7 +244,7 @@ static Uri GenerateImageUrl(int width, int height, int seed) var image = new ImageObject { Url = GenerateImageUrl(1024, 724, imageSeed), - Thumbnail = GetRandomElementsOf(faker, thumbnails, isGoldenRecord, 0, 1).ToList() + Thumbnail = GetRandomElementsOf(faker, thumbnails, isGoldenRecord, 1, 1).ToList() }; images.Add(image); } From a025713b181749fe4e42000729416651e55a3821 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 21 Sep 2023 16:36:06 +0100 Subject: [PATCH 05/17] fix typo --- Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index 7f19a996..6a6a6595 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -269,7 +269,7 @@ private QuantitativeValue GenerateAgeRange(Faker faker, bool isGoldenRecord) { var ageRange = new QuantitativeValue(); if (isGoldenRecord || faker.Random.Bool()) ageRange.MaxValue = faker.Random.Number(16, 100); - if (isGoldenRecord || faker.Random.Bool()) ageRange.MinValue = faker.Random.Number(0, ageRange.MaxValue == null ? (int)ageRange.MaxValue : 100); + if (isGoldenRecord || faker.Random.Bool()) ageRange.MinValue = faker.Random.Number(0, ageRange.MaxValue == null ? 100 : (int)ageRange.MaxValue); if (ageRange.MaxValue == null && ageRange.MinValue == null) ageRange.MinValue = 0; return ageRange; From 9040df2f97577d1769983e1caaad0875907c216c Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 21 Sep 2023 17:34:44 +0100 Subject: [PATCH 06/17] update framework from cire --- .../Feeds/FacilitiesFeeds.cs | 358 +++++++++----- .../Feeds/SessionsFeeds.cs | 456 +++++++++++++----- .../Settings/EngineConfig.cs | 3 + .../Stores/FacilityStore.cs | 9 +- .../Stores/IdempotencyStore.cs | 25 + .../Stores/SessionStore.cs | 4 +- 6 files changed, 612 insertions(+), 243 deletions(-) create mode 100644 Examples/BookingSystem.AspNetFramework/Stores/IdempotencyStore.cs diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs index e348d1e9..69cb9c4b 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs @@ -1,4 +1,6 @@ -using OpenActive.DatasetSite.NET; +using Bogus; +using BookingSystem.AspNetCore.Helpers; +using OpenActive.DatasetSite.NET; using OpenActive.FakeDatabase.NET; using OpenActive.NET; using OpenActive.NET.Rpde.Version1; @@ -43,29 +45,134 @@ protected override async Task>> GetRpdeItems(long? af var query = db .SelectMulti(q) - .Select(result => new RpdeItem + .Select(result => { - Kind = RpdeKind.FacilityUse, - Id = result.Item1.Id, - Modified = result.Item1.Modified, - State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, - Data = result.Item1.Deleted ? null : new FacilityUse + var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; + var isGoldenRecord = faker.Random.Bool(); + + return new RpdeItem { - // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on - // the parent class? Current thinking is it's more extensible on parent class as function signature remains - // constant as power of configuration through underlying class grows (i.e. as new properties are added) - Id = RenderOpportunityId(new FacilityOpportunity + Kind = RpdeKind.FacilityUse, + Id = result.Item1.Id, + Modified = result.Item1.Modified, + State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, + Data = result.Item1.Deleted ? null : new FacilityUse { - OpportunityType = OpportunityType.FacilityUse, // isIndividual?? - FacilityUseId = result.Item1.Id - }), - Name = result.Item1.Name, - Provider = _appSettings.FeatureFlags.SingleSeller ? new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List + // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on + // the parent class? Current thinking is it's more extensible on parent class as function signature remains + // constant as power of configuration through underlying class grows (i.e. as new properties are added) + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = OpportunityType.FacilityUse, // isIndividual?? + FacilityUseId = result.Item1.Id + }), + Identifier = result.Item1.Id, + Name = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Name, + Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), + Provider = GenerateOrganizer(result.Item2), + Url = new Uri($"https://www.example.com/facilities/{result.Item1.Id}"), + AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), + AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), + AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)), + IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), + Category = GenerateCategory(faker, isGoldenRecord), + Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord), + Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null, + Location = FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), + FacilityType = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Facility, + 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, + } + }; + }); + + return query.ToList(); + } + } + + private (string Name, List Facility) GetNameAndFacilityTypeForFacility(string databaseTitle, bool isGoldenRecord) + { + // If both FACILITY_TYPE_ID and FACILITY_TYPE_PREF_LABEL env vars are set, these override the randomly generated activity. We also use these to generate an appropriate name + if (Environment.GetEnvironmentVariable("FACILITY_TYPE_ID") != null && Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL") != null) + { + var name = $"{(isGoldenRecord ? "GOLDEN: " : "")} {Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL")} facility"; + var concept = new Concept + { + Id = new Uri(Environment.GetEnvironmentVariable("FACILITY_TYPE_ID")), + PrefLabel = Environment.GetEnvironmentVariable("FACILITY_TYPE_PREF_LABEL"), + InScheme = new Uri("https://openactive.io/activity-list") + }; + + return (name, new List { concept }); + } + + // If there isn't an override, we use the randomly generated name to derive the appropriate activity + Concept facilityConcept; + switch (databaseTitle) + { + case string a when a.Contains("Sports Hall"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#da364f9b-8bb2-490e-9e2f-1068790b9e35"), + PrefLabel = "Sports Hall", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + case string a when a.Contains("Squash Court"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + case string a when a.Contains("Badminton Court"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#9db5681e-700e-4b30-99a5-355885d94db2"), + PrefLabel = "Badminton Court", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + case string a when a.Contains("Cricket Net"): + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#2d333183-6a6d-4a95-aad4-c5699f705b14"), + PrefLabel = "Cricket Net", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + default: + facilityConcept = new Concept + { + Id = new Uri("https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651"), + PrefLabel = "Squash Court", + InScheme = new Uri("https://openactive.io/facility-types") + }; + break; + } + + var nameWithGolden = $"{(isGoldenRecord ? "GOLDEN: " : "")}{databaseTitle}"; + return (nameWithGolden, new List { facilityConcept }); + + } + + private Organization GenerateOrganizer(SellerTable seller) + { + return _appSettings.FeatureFlags.SingleSeller ? new Organization + { + Id = RenderSingleSellerId(), + Name = "Test Seller", + TaxMode = TaxMode.TaxGross, + TermsOfService = new List { new PrivacyPolicy { @@ -74,13 +181,13 @@ protected override async Task>> GetRpdeItems(long? af RequiresExplicitConsent = false } }, - IsOpenBookingAllowed = true, - } : new Organization - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }), - Name = result.Item2.Name, - TaxMode = result.Item2.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List + IsOpenBookingAllowed = true, + } : new Organization + { + Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), + Name = seller.Name, + TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, + TermsOfService = new List { new PrivacyPolicy { @@ -89,33 +196,33 @@ protected override async Task>> GetRpdeItems(long? af RequiresExplicitConsent = false } }, - IsOpenBookingAllowed = true, - }, - Location = _fakeBookingSystem.Database.GetPlaceById(result.Item1.PlaceId), - Url = new Uri("https://www.example.com/a-session-age"), - FacilityType = new List { - new Concept - { - Id = new Uri(facilityTypeId), - 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, - } - }); + IsOpenBookingAllowed = true, + }; + } - return query.ToList(); - } + private List GenerateCategory(Faker faker, bool isGoldenRecord) + { + var listOfPossibleCategories = new List + { + "Bookable Facilities", + "Ball Sports", + }; + + return FeedGenerationHelper.GetRandomElementsOf(faker, listOfPossibleCategories, isGoldenRecord, 1).ToList(); + } + + private List GenerateOpeningHours(Faker faker) + { + return new List + { + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Sunday }, Opens = $"{faker.Random.Number(9,12)}:00", Closes = $"{faker.Random.Number(15,17)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Monday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Tuesday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Wednesday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Thursday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Friday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, + new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Saturday }, Opens = $"{faker.Random.Number(9,12)}:00", Closes = $"{faker.Random.Number(15,17)}:30"} + }; } } @@ -144,96 +251,91 @@ protected override async Task>> GetRpdeItems(long? afterTime x.Modified == afterTimestamp && x.Id > afterId && x.Modified < (DateTimeOffset.UtcNow - new TimeSpan(0, 0, 2)).UtcTicks) .Take(RpdePageSize) - .Select(x => new RpdeItem + .Select(x => { - Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, - Id = x.Id, - Modified = x.Modified, - State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, - Data = x.Deleted ? null : new Slot + var faker = new Faker() { Random = new Randomizer(((int)x.Modified + (int)x.Id)) }; + return new RpdeItem { - // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on - // the parent class? Current thinking is it's more extensible on parent class as function signature remains - // constant as power of configuration through underlying class grows (i.e. as new properties are added) - Id = RenderOpportunityId(new FacilityOpportunity - { - OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, - FacilityUseId = x.FacilityUseId, - SlotId = x.Id, - IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, - }), - FacilityUse = _appSettings.FeatureFlags.FacilityUseHasSlots ? - RenderOpportunityId(new FacilityOpportunity + Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, + Id = x.Id, + Modified = x.Modified, + State = x.Deleted ? RpdeState.Deleted : RpdeState.Updated, + Data = x.Deleted ? null : new Slot { - 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, - EndDate = (DateTimeOffset)x.End, - Duration = x.End - x.Start, - RemainingUses = x.RemainingUses - x.LeasedUses, - MaximumUses = x.MaximumUses, - Offers = new List { new Offer - { - Id = RenderOfferId(new FacilityOpportunity - { - OfferId = 0, - OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, - FacilityUseId = x.FacilityUseId, - SlotId = x.Id, - IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, - }), - Price = x.Price, - PriceCurrency = "GBP", - OpenBookingFlowRequirement = OpenBookingFlowRequirement(x), - ValidFromBeforeStartDate = x.ValidFromBeforeStartDate, - LatestCancellationBeforeStartDate = x.LatestCancellationBeforeStartDate, - OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : x.Prepayment.Convert(), - AllowCustomerCancellationFullRefund = x.AllowCustomerCancellationFullRefund, - } - }, - } + // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on + // the parent class? Current thinking is it's more extensible on parent class as function signature remains + // constant as power of configuration through underlying class grows (i.e. as new properties are added) + Id = RenderOpportunityId(new FacilityOpportunity + { + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, + FacilityUseId = x.FacilityUseId, + SlotId = x.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? x.IndividualFacilityUseId : null, + }), + 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, + EndDate = (DateTimeOffset)x.End, + Duration = x.End - x.Start, + RemainingUses = x.RemainingUses - x.LeasedUses, + MaximumUses = x.MaximumUses, + Offers = GenerateOffers(faker, false, x) + } + }; }); return query.ToList(); } } - private static List OpenBookingFlowRequirement(SlotTable slot) + private List GenerateOffers(Faker faker, bool isGoldenRecord, SlotTable slot) { - List openBookingFlowRequirement = null; - - if (slot.RequiresApproval) + var ageRangesForOffers = new List { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingApproval); - } + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult"}, + new QuantitativeValue { MaxValue = 17, Name = "Junior"}, + new QuantitativeValue {MinValue = 60, Name = "Senior"}, + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult (off-peak)"}, + }; - if (slot.RequiresAttendeeValidation) + Offer GenerateOffer(SlotTable slot, QuantitativeValue ageRange) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingAttendeeDetails); + return new Offer + { + Id = RenderOfferId(new FacilityOpportunity + { + OfferId = 0, + OpportunityType = _appSettings.FeatureFlags.FacilityUseHasSlots ? OpportunityType.FacilityUseSlot : OpportunityType.IndividualFacilityUseSlot, + FacilityUseId = slot.FacilityUseId, + SlotId = slot.Id, + IndividualFacilityUseId = !_appSettings.FeatureFlags.FacilityUseHasSlots ? slot.IndividualFacilityUseId : null, + }), + Price = slot.Price, + PriceCurrency = "GBP", + OpenBookingFlowRequirement = FeedGenerationHelper.OpenBookingFlowRequirement(slot.RequiresApproval, slot.RequiresAttendeeValidation, slot.RequiresAdditionalDetails, slot.AllowsProposalAmendment), + ValidFromBeforeStartDate = slot.ValidFromBeforeStartDate, + LatestCancellationBeforeStartDate = slot.LatestCancellationBeforeStartDate, + OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : slot.Prepayment.Convert(), + AllowCustomerCancellationFullRefund = slot.AllowCustomerCancellationFullRefund, + AcceptedPaymentMethod = new List { PaymentMethod.Cash, PaymentMethod.PaymentMethodCreditCard }, + }; } - if (slot.RequiresAdditionalDetails) - { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingIntakeForm); - } + var allOffersForAllAgeRanges = ageRangesForOffers.Select(ageRange => GenerateOffer(slot, ageRange)).ToList(); - if (slot.AllowsProposalAmendment) - { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingNegotiation); - } - return openBookingFlowRequirement; + return FeedGenerationHelper.GetRandomElementsOf(faker, allOffersForAllAgeRanges, isGoldenRecord, 1, 4).ToList(); } + } } \ No newline at end of file diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs index b803b921..6a6a6595 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs @@ -8,6 +8,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bogus; +using BookingSystem.AspNetCore.Helpers; +using ServiceStack; +using System.Globalization; namespace BookingSystem { @@ -85,9 +89,6 @@ 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() @@ -102,140 +103,377 @@ protected override async Task>> GetRpdeItems(long? var query = db .SelectMulti(q) - .Select(result => new RpdeItem + .Select(result => { - Kind = RpdeKind.SessionSeries, - Id = result.Item1.Id, - Modified = result.Item1.Modified, - State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, - Data = result.Item1.Deleted ? null : new SessionSeries + var intt = (int)result.Item1.Modified; + + var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; + // here we randomly decide whether the item is going to be a golden record or not by using Faker + // See the README for more detail on golden records. + var isGoldenRecord = faker.Random.Bool(); + + return new RpdeItem { - // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on - // the parent class? Current thinking is it's more extensible on parent class as function signature remains - // constant as power of configuration through underlying class grows (i.e. as new properties are added) - Id = RenderOpportunityId(new SessionOpportunity - { - OpportunityType = OpportunityType.SessionSeries, - SessionSeriesId = result.Item1.Id - }), - Name = result.Item1.Title, - EventAttendanceMode = MapAttendanceMode(result.Item1.AttendanceMode), - Organizer = _appSettings.FeatureFlags.SingleSeller ? new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - } : result.Item2.IsIndividual ? (ILegalEntity)new Person - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }), - Name = result.Item2.Name, - TaxMode = result.Item2.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - IsOpenBookingAllowed = true, - } : (ILegalEntity)new Organization + Kind = RpdeKind.SessionSeries, + Id = result.Item1.Id, + Modified = result.Item1.Modified, + State = result.Item1.Deleted ? RpdeState.Deleted : RpdeState.Updated, + Data = result.Item1.Deleted ? null : new SessionSeries { - Id = RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }), - Name = result.Item2.Name, - TaxMode = result.Item2.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List + // QUESTION: Should the this.IdTemplate and this.BaseUrl be passed in each time rather than set on + // the parent class? Current thinking is it's more extensible on parent class as function signature remains + // constant as power of configuration through underlying class grows (i.e. as new properties are added) + Id = RenderOpportunityId(new SessionOpportunity { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - }, - Offers = new List { new Offer - { - Id = RenderOfferId(new SessionOpportunity - { - OfferOpportunityType = OpportunityType.SessionSeries, - SessionSeriesId = result.Item1.Id, - OfferId = 0 - }), - Price = result.Item1.Price, - PriceCurrency = "GBP", - OpenBookingFlowRequirement = OpenBookingFlowRequirement(result.Item1), - ValidFromBeforeStartDate = result.Item1.ValidFromBeforeStartDate, - LatestCancellationBeforeStartDate = result.Item1.LatestCancellationBeforeStartDate, - OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : result.Item1.Prepayment.Convert(), - AllowCustomerCancellationFullRefund = result.Item1.AllowCustomerCancellationFullRefund - } - }, - 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 - { - new Concept - { - Id = new Uri(activityId), - PrefLabel = activityPrefLabel, - InScheme = new Uri("https://openactive.io/activity-list") - } + OpportunityType = OpportunityType.SessionSeries, + SessionSeriesId = result.Item1.Id + }), + Identifier = result.Item1.Id, + Name = GetNameAndActivityForSessions(result.Item1.Title, isGoldenRecord).Name, + EventAttendanceMode = MapAttendanceMode(result.Item1.AttendanceMode), + Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), + AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), + GenderRestriction = faker.Random.Enum(), + AgeRange = GenerateAgeRange(faker, isGoldenRecord), + Level = faker.Random.ListItems(new List { "Beginner", "Intermediate", "Advanced" }, 1).ToList(), + Organizer = GenerateOrganizerOrPerson(faker, result.Item2), + AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), + AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)), + IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), + Category = GenerateCategory(faker, isGoldenRecord), + Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord), + Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null, + Leader = GenerateListOfPersons(faker, isGoldenRecord, 2), + Contributor = GenerateListOfPersons(faker, isGoldenRecord, 2), + IsCoached = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), + Offers = GenerateOffers(faker, isGoldenRecord, result.Item1), + // location MUST not be provided for fully virtual sessions + Location = result.Item1.AttendanceMode == AttendanceMode.Online ? null : FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), + // beta:affiliatedLocation MAY be provided for fully virtual sessions + AffiliatedLocation = (result.Item1.AttendanceMode == AttendanceMode.Offline && faker.Random.Bool()) ? null : FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), + EventSchedule = GenerateSchedules(faker, isGoldenRecord), + SchedulingNote = GenerateSchedulingNote(faker, isGoldenRecord), + IsAccessibleForFree = result.Item1.Price == 0, + Url = new Uri($"https://www.example.com/sessions/{result.Item1.Id}"), + Activity = GetNameAndActivityForSessions(result.Item1.Title, isGoldenRecord).Activity, + Programme = GenerateBrand(faker, isGoldenRecord), + IsInteractivityPreferred = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })), + IsVirtuallyCoached = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })), + ParticipantSuppliedEquipment = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? OpenActive.NET.RequiredStatusType.Optional : faker.Random.ListItem(new List { OpenActive.NET.RequiredStatusType.Optional, OpenActive.NET.RequiredStatusType.Required, OpenActive.NET.RequiredStatusType.Unavailable, null })), } - } - }); ; + }; + }); return query.ToList(); } } + private static EventAttendanceModeEnumeration MapAttendanceMode(AttendanceMode attendanceMode) + { + switch (attendanceMode) + { + case AttendanceMode.Offline: + return EventAttendanceModeEnumeration.OfflineEventAttendanceMode; + case AttendanceMode.Online: + return EventAttendanceModeEnumeration.OnlineEventAttendanceMode; + case AttendanceMode.Mixed: + return EventAttendanceModeEnumeration.MixedEventAttendanceMode; + default: + throw new OpenBookingException(new OpenBookingError(), $"AttendanceMode Type {attendanceMode} not supported"); + } + } + + private (string Name, List Activity) GetNameAndActivityForSessions(string databaseTitle, bool isGoldenRecord) + { + // If both ACTIVITY_ID and ACTIVITY_PREF_LABEL env vars are set, these override the randomly generated activity. We also use these to generate an appropriate name + if (Environment.GetEnvironmentVariable("ACTIVITY_ID") != null && Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL") != null) + { + var name = $"{(isGoldenRecord ? "GOLDEN: " : "")} {Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL")} class"; + var concept = new Concept + { + Id = new Uri(Environment.GetEnvironmentVariable("ACTIVITY_ID")), + PrefLabel = Environment.GetEnvironmentVariable("ACTIVITY_PREF_LABEL"), + InScheme = new Uri("https://openactive.io/activity-list") + }; + + return (name, new List { concept }); + } + + // If there isn't an override, we use the randomly generated name to derive the appropriate activity + Concept activityConcept; + switch (databaseTitle) + { + case string a when a.Contains("Yoga"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#bf1a5e00-cdcf-465d-8c5a-6f57040b7f7e"), + PrefLabel = "Yoga", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Zumba"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#78503fa2-ed24-4a80-a224-e2e94581d8a8"), + PrefLabel = "Zumba®", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Walking"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#95092977-5a20-4d6e-b312-8fddabe71544"), + PrefLabel = "Walking", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Cycling"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#4a19873e-118e-43f4-b86e-05acba8fb1de"), + PrefLabel = "Cycling", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Running"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#72ddb2dc-7d75-424e-880a-d90eabe91381"), + PrefLabel = "Running", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + case string a when a.Contains("Jumping"): + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#8a4abff3-c616-4f33-80a1-398b88c672a3"), + PrefLabel = "World Jumping®", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + default: + activityConcept = new Concept + { + Id = new Uri("https://openactive.io/activity-list#c07d63a0-8eb9-4602-8bcc-23be6deb8f83"), + PrefLabel = "Jet Skiing", + InScheme = new Uri("https://openactive.io/activity-list") + }; + break; + } + + var nameWithGolden = $"{(isGoldenRecord ? "GOLDEN: " : "")}{databaseTitle}"; + return (nameWithGolden, new List { activityConcept }); + + } + + private QuantitativeValue GenerateAgeRange(Faker faker, bool isGoldenRecord) + { + var ageRange = new QuantitativeValue(); + if (isGoldenRecord || faker.Random.Bool()) ageRange.MaxValue = faker.Random.Number(16, 100); + if (isGoldenRecord || faker.Random.Bool()) ageRange.MinValue = faker.Random.Number(0, ageRange.MaxValue == null ? 100 : (int)ageRange.MaxValue); + + if (ageRange.MaxValue == null && ageRange.MinValue == null) ageRange.MinValue = 0; + return ageRange; + } - private static List OpenBookingFlowRequirement(ClassTable @class) + private ILegalEntity GenerateOrganizerOrPerson(Faker faker, SellerTable seller) { - List openBookingFlowRequirement = null; + if (_appSettings.FeatureFlags.SingleSeller) + return new Organization + { + Id = RenderSingleSellerId(), + Name = "Test Seller", + TaxMode = TaxMode.TaxGross, + TermsOfService = new List + { + new PrivacyPolicy + { + Name = "Privacy Policy", + Url = new Uri("https://example.com/privacy.html"), + RequiresExplicitConsent = false + } + }, + IsOpenBookingAllowed = true, + Telephone = faker.Phone.PhoneNumber("0#### ######"), + SameAs = new List { new Uri("https://socialmedia/testseller") } + }; + if (seller.IsIndividual) + return new OpenActive.NET.Person + { + Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), + Name = seller.Name, + TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, + IsOpenBookingAllowed = true, + Telephone = faker.Phone.PhoneNumber("07### ######") + }; + return new Organization + { + Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), + Name = seller.Name, + TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, + TermsOfService = new List + { + new PrivacyPolicy + { + Name = "Privacy Policy", + Url = new Uri("https://example.com/privacy.html"), + RequiresExplicitConsent = false + } + }, + IsOpenBookingAllowed = true, + Url = new Uri(faker.Internet.Url()), + Telephone = faker.Phone.PhoneNumber("0#### ######"), + SameAs = new List { new Uri($"https://socialmedia/{seller.Name}") } + }; + } - if (@class.RequiresApproval) + private List GenerateCategory(Faker faker, bool isGoldenRecord) + { + var listOfPossibleCategories = new List { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingApproval); + "Group Exercise Classes", + "Toning & Strength", + "Group Exercise - Virtual" + }; + + return FeedGenerationHelper.GetRandomElementsOf(faker, listOfPossibleCategories, isGoldenRecord, 1).ToList(); + } + + private List GenerateListOfPersons(Faker faker, bool isGoldenRecord, int possibleMax) + { + static OpenActive.NET.Person GeneratePerson(Faker faker, bool isGoldenRecord) + { + var id = faker.Finance.Bic(); + var genderIndex = faker.Random.Number(1); + var gender = (Bogus.DataSets.Name.Gender)genderIndex; + var givenName = faker.Name.FirstName(gender); + var familyName = faker.Name.LastName(gender); + var name = $"{givenName} {familyName}"; + var isLiteRecord = isGoldenRecord ? false : faker.Random.Bool(); + + return new OpenActive.NET.Person + { + Id = new Uri($"https://example.com/people/{id}"), + Identifier = id, + Name = name, + GivenName = isLiteRecord ? null : givenName, + FamilyName = isLiteRecord ? null : familyName, + Gender = genderIndex == 1 ? Schema.NET.GenderType.Female : Schema.NET.GenderType.Male, + JobTitle = faker.Random.ListItem(new List { "Leader", "Team leader", "Host", "Instructor", "Coach" }), + Telephone = isLiteRecord ? null : faker.Phone.PhoneNumber("07## ### ####"), + Email = isLiteRecord ? null : faker.Internet.ExampleEmail(), + Url = new Uri($"{faker.Internet.Url()}/profile/{faker.Random.Number(50)}"), + Image = new Schema.NET.ImageObject { Url = new Uri(faker.Internet.Avatar()) } + }; } - if (@class.RequiresAttendeeValidation) + var output = new List(); + var max = isGoldenRecord ? possibleMax : faker.Random.Number(possibleMax); + for (var i = 0; i < max; i++) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingAttendeeDetails); + output.Add(GeneratePerson(faker, isGoldenRecord)); } + return output; + } - if (@class.RequiresAdditionalDetails) + private List GenerateSchedules(Faker faker, bool isGoldenRecord) + { + var schedules = new List(); + PartialSchedule GenerateSchedule(Faker faker) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingIntakeForm); + var startTimeString = $"{faker.Random.Number(min: 10, max: 22)}:{faker.Random.ListItem(new List { "00", "15", "30", "45" })}:00"; + var startTime = new TimeValue(startTimeString); + var duration = faker.Random.ListItem(new List { new TimeSpan(0, 30, 0), new TimeSpan(1, 0, 0), new TimeSpan(1, 30, 0), new TimeSpan(2, 0, 0) }); + var startTimeSpan = TimeSpan.Parse(startTimeString); + + var endTime = new DateTime(startTimeSpan.Add(duration).Ticks); + var endTimeString = endTime.ToString("HH:mm"); + var endTimeTM = new TimeValue(endTimeString); + var startDateFaker = faker.Date.Soon(); + var startDate = new DateValue(startDateFaker); + var endDate = new DateValue(faker.Date.Soon(28, startDateFaker)); + + var partialSchedule = new PartialSchedule + { + StartTime = startTime, + Duration = duration, + EndTime = endTimeTM, + StartDate = startDate, + EndDate = endDate, + RepeatFrequency = faker.Random.ListItem(new List { new TimeSpan(7, 0, 0, 0), new TimeSpan(14, 0, 0, 0) }), + ByDay = faker.Random.EnumValues().ToList() + }; + return partialSchedule; } - if (@class.AllowsProposalAmendment) + for (var i = 0; i < 2; i++) { - openBookingFlowRequirement = openBookingFlowRequirement ?? new List(); - openBookingFlowRequirement.Add(OpenActive.NET.OpenBookingFlowRequirement.OpenBookingNegotiation); + schedules.Add(GenerateSchedule(faker)); } - return openBookingFlowRequirement; + + return FeedGenerationHelper.GetRandomElementsOf(faker, schedules, isGoldenRecord, 1, 1).ToList(); } - private static EventAttendanceModeEnumeration MapAttendanceMode(AttendanceMode attendanceMode) + private string GenerateSchedulingNote(Faker faker, bool isGoldenRecord) { - switch (attendanceMode) + var allSchedulingNotes = new List { - case AttendanceMode.Offline: - return EventAttendanceModeEnumeration.OfflineEventAttendanceMode; - case AttendanceMode.Online: - return EventAttendanceModeEnumeration.OnlineEventAttendanceMode; - case AttendanceMode.Mixed: - return EventAttendanceModeEnumeration.MixedEventAttendanceMode; - default: - throw new OpenBookingException(new OpenBookingError(), $"AttendanceMode Type {attendanceMode} not supported"); + "Sessions are not running during school holidays.", + "Sessions may be cancelled with 15 minutes notice, please keep an eye on your e-mail.", + "Sessions are scheduled with best intentions, but sometimes need to be rescheduled due to venue availability. Ensure that you contact the organizer before turning up." + }; + + if (isGoldenRecord) return faker.Random.ListItem(allSchedulingNotes); + return faker.Random.Bool() ? faker.Random.ListItem(allSchedulingNotes) : null; + } + + private List GenerateOffers(Faker faker, bool isGoldenRecord, ClassTable @class) + { + var ageRangesForOffers = new List + { + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult"}, + new QuantitativeValue { MaxValue = 17, Name = "Junior"}, + new QuantitativeValue {MinValue = 60, Name = "Senior"}, + new QuantitativeValue {MinValue = 18, MaxValue = 59, Name = "Adult (off-peak)"}, + }; + + Offer GenerateOffer(ClassTable @class, QuantitativeValue ageRange) + { + return new Offer + { + Id = RenderOfferId(new SessionOpportunity + { + OfferOpportunityType = OpportunityType.SessionSeries, + SessionSeriesId = @class.Id, + OfferId = 0 + }), + Price = @class.Price, + PriceCurrency = "GBP", + Name = ageRange.Name, + OpenBookingFlowRequirement = FeedGenerationHelper.OpenBookingFlowRequirement(@class.RequiresApproval, @class.RequiresAttendeeValidation, @class.RequiresAdditionalDetails, @class.AllowsProposalAmendment), + ValidFromBeforeStartDate = @class.ValidFromBeforeStartDate, + LatestCancellationBeforeStartDate = @class.LatestCancellationBeforeStartDate, + OpenBookingPrepayment = _appSettings.FeatureFlags.PrepaymentAlwaysRequired ? null : @class.Prepayment.Convert(), + AllowCustomerCancellationFullRefund = @class.AllowCustomerCancellationFullRefund, + AcceptedPaymentMethod = new List { PaymentMethod.Cash, PaymentMethod.PaymentMethodCreditCard }, + AgeRestriction = ageRange, + }; } + + var allOffersForAllAgeRanges = ageRangesForOffers.Select(ageRange => GenerateOffer(@class, ageRange)).ToList(); + + return FeedGenerationHelper.GetRandomElementsOf(faker, allOffersForAllAgeRanges, isGoldenRecord, 1, 2).ToList(); + } + + private Brand GenerateBrand(Faker faker, bool isGoldenRecord) + { + return new Brand + { + Name = faker.Random.ListItem(new List { "Keyways Active", "This Girl Can", "Back to Activity", "Mega-active Super Dads" }), + Url = new Uri(faker.Internet.Url()), + Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), + Logo = new ImageObject { Url = new Uri(faker.Internet.Avatar()) }, + Video = new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=N268gBOvnzo") } } + }; } } } diff --git a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs index 5dafa9fa..70ab090c 100644 --- a/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs +++ b/Examples/BookingSystem.AspNetFramework/Settings/EngineConfig.cs @@ -166,6 +166,9 @@ public static StoreBookingEngine CreateStoreBookingEngine(AppSettings appSetting ), HasSingleSeller = appSettings.FeatureFlags.SingleSeller, + // IdempotencyStore used for storing the response to Order Creation B/P requests + IdempotencyStore = new AcmeIdempotencyStore(), + OpenDataFeeds = new Dictionary { { OpportunityType.ScheduledSession, new AcmeScheduledSessionRpdeGenerator(fakeBookingSystem) diff --git a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs index 142c459f..a8381c34 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/FacilityStore.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BookingSystem.AspNetCore.Helpers; using OpenActive.DatasetSite.NET; using OpenActive.FakeDatabase.NET; using OpenActive.NET; @@ -320,7 +321,7 @@ protected override async Task GetOrderItems(List { new Concept { @@ -374,11 +375,11 @@ protected override async Task GetOrderItems(List GetSuccessfulOrderCreationResponse(string idempotencyKey) + { + return new ValueTask((string)_cache.Get(idempotencyKey)); + } + + protected override ValueTask SetSuccessfulOrderCreationResponse(string idempotencyKey, string responseJson) + { + var policy = new CacheItemPolicy(); + policy.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(5); + _cache.Set(idempotencyKey, responseJson, policy); + return new ValueTask(); + } + } +} \ No newline at end of file diff --git a/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs b/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs index ff596c47..318cc82b 100644 --- a/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs +++ b/Examples/BookingSystem.AspNetFramework/Stores/SessionStore.cs @@ -8,7 +8,7 @@ using OpenActive.FakeDatabase.NET; using RequiredStatusType = OpenActive.FakeDatabase.NET.RequiredStatusType; using System.Threading.Tasks; - +using BookingSystem.AspNetCore.Helpers; namespace BookingSystem { @@ -361,7 +361,7 @@ protected override async Task GetOrderItems(List { new Concept From b3fb02d309aba87dd51e1027b4cf289d736d9be0 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 13 Mar 2024 16:00:21 +0000 Subject: [PATCH 07/17] remove modified from faker seed --- Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs | 4 ++-- Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 69cb9c4b..855bc7db 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -47,7 +47,7 @@ protected override async Task>> GetRpdeItems(long? af .SelectMulti(q) .Select(result => { - var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; + var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; var isGoldenRecord = faker.Random.Bool(); return new RpdeItem @@ -253,7 +253,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => { - var faker = new Faker() { Random = new Randomizer(((int)x.Modified + (int)x.Id)) }; + var faker = new Faker() { Random = new Randomizer((int)x.Id) }; return new RpdeItem { Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index 6a6a6595..e5651fab 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -107,7 +107,7 @@ protected override async Task>> GetRpdeItems(long? { var intt = (int)result.Item1.Modified; - var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; + var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; // here we randomly decide whether the item is going to be a golden record or not by using Faker // See the README for more detail on golden records. var isGoldenRecord = faker.Random.Bool(); From 1bf5b1071f4aa7d7dcc5286f27adec764bca0ab8 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 13 Mar 2024 16:11:30 +0000 Subject: [PATCH 08/17] update Node version for CI --- .github/workflows/openactive-test-suite.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/openactive-test-suite.yml b/.github/workflows/openactive-test-suite.yml index b8d20c3b..891d1bc2 100644 --- a/.github/workflows/openactive-test-suite.yml +++ b/.github/workflows/openactive-test-suite.yml @@ -63,10 +63,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.419 - - name: Setup Node.js 14.x + - name: Setup Node.js 18.17.1 uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.17.1 - name: Install OpenActive.Server.NET dependencies if: ${{ matrix.profile != 'no-auth' && matrix.profile != 'single-seller' }} run: dotnet restore ./server/ @@ -136,10 +136,10 @@ jobs: repository: openactive/openactive-test-suite ref: ${{ steps.refs.outputs.mirror_ref }} path: tests - - name: Setup Node.js 14.x + - name: Setup Node.js 18.17.1 uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.17.1 - name: Setup MSBuild path uses: microsoft/setup-msbuild@v1.0.2 - name: Setup NuGet From 1434c677cd911330721508845520024cc8413770 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 14 Mar 2024 15:05:25 +0000 Subject: [PATCH 09/17] fix feed generation --- .../Feeds/FacilitiesFeeds.cs | 42 +++------------ .../Feeds/SessionsFeeds.cs | 42 ++------------- .../Helpers/FeedGenerationHelper.cs | 53 ++++++++++++++++++- 3 files changed, 61 insertions(+), 76 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 855bc7db..9bf1b691 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -69,7 +69,12 @@ protected override async Task>> GetRpdeItems(long? af Identifier = result.Item1.Id, Name = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Name, Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), - Provider = GenerateOrganizer(result.Item2), + Provider = FeedGenerationHelper.GenerateOrganization( + faker, + result.Item2, + _appSettings.FeatureFlags.SingleSeller, + _appSettings.FeatureFlags.SingleSeller ? RenderSingleSellerId() : RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }) + ), Url = new Uri($"https://www.example.com/facilities/{result.Item1.Id}"), AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), @@ -165,41 +170,6 @@ protected override async Task>> GetRpdeItems(long? af } - private Organization GenerateOrganizer(SellerTable seller) - { - return _appSettings.FeatureFlags.SingleSeller ? new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - } : new Organization - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), - Name = seller.Name, - TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - }; - } - private List GenerateCategory(Faker faker, bool isGoldenRecord) { var listOfPossibleCategories = new List diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index e5651fab..3ab86dea 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -277,25 +277,6 @@ private QuantitativeValue GenerateAgeRange(Faker faker, bool isGoldenRecord) private ILegalEntity GenerateOrganizerOrPerson(Faker faker, SellerTable seller) { - if (_appSettings.FeatureFlags.SingleSeller) - return new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - Telephone = faker.Phone.PhoneNumber("0#### ######"), - SameAs = new List { new Uri("https://socialmedia/testseller") } - }; if (seller.IsIndividual) return new OpenActive.NET.Person { @@ -305,25 +286,8 @@ private ILegalEntity GenerateOrganizerOrPerson(Faker faker, SellerTable seller) IsOpenBookingAllowed = true, Telephone = faker.Phone.PhoneNumber("07### ######") }; - return new Organization - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), - Name = seller.Name, - TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - Url = new Uri(faker.Internet.Url()), - Telephone = faker.Phone.PhoneNumber("0#### ######"), - SameAs = new List { new Uri($"https://socialmedia/{seller.Name}") } - }; + var organizationId = _appSettings.FeatureFlags.SingleSeller ? RenderSingleSellerId() : RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }); + return FeedGenerationHelper.GenerateOrganization(faker, seller, _appSettings.FeatureFlags.SingleSeller, organizationId); } private List GenerateCategory(Faker faker, bool isGoldenRecord) @@ -367,7 +331,7 @@ static OpenActive.NET.Person GeneratePerson(Faker faker, bool isGoldenRecord) } var output = new List(); - var max = isGoldenRecord ? possibleMax : faker.Random.Number(possibleMax); + var max = isGoldenRecord ? possibleMax : faker.Random.Number(1, possibleMax); for (var i = 0; i < max; i++) { output.Add(GeneratePerson(faker, isGoldenRecord)); diff --git a/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs b/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs index f65e956d..cc099062 100644 --- a/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs +++ b/Examples/BookingSystem.AspNetCore/Helpers/FeedGenerationHelper.cs @@ -4,6 +4,11 @@ using OpenActive.FakeDatabase.NET; using Bogus; using System.Linq; +using Bogus.DataSets; +using OpenActive.Server.NET.OpenBookingHelper; +using System.Security.Policy; + + namespace BookingSystem.AspNetCore.Helpers { @@ -231,7 +236,7 @@ static Uri GenerateImageUrl(int width, int height, int seed) } var images = new List(); - var min = isGoldenRecord ? 4 : 0; + var min = isGoldenRecord ? 4 : 1; var imageCount = faker.Random.Number(min, 3); for (var i = 0; i < imageCount; i++) { @@ -250,6 +255,52 @@ static Uri GenerateImageUrl(int width, int height, int seed) } return images; } + + public static Organization GenerateOrganization(Faker faker, SellerTable seller, bool isSingleSeller, Uri organizationId) + { + if (isSingleSeller) + return new Organization + { + Id = organizationId, + Name = "Test Seller", + TaxMode = TaxMode.TaxGross, + TermsOfService = new List + { + new PrivacyPolicy + { + Name = "Privacy Policy", + Url = new Uri("https://example.com/privacy.html"), + RequiresExplicitConsent = false + } + }, + IsOpenBookingAllowed = true, + Telephone = faker.Phone.PhoneNumber("0#### ######"), + SameAs = new List { new Uri("https://socialmedia.com/testseller") } + }; + + return new Organization + { + Id = organizationId, + Name = seller.Name, + TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, + TermsOfService = new List + { + new PrivacyPolicy + { + Name = "Privacy Policy", + Url = new Uri("https://example.com/privacy.html"), + RequiresExplicitConsent = false + } + }, + IsOpenBookingAllowed = true, + Url = new Uri(faker.Internet.Url()), + Telephone = faker.Phone.PhoneNumber("0#### ######"), + SameAs = new List { new Uri($"https://socialmedia.com/{seller.Name.Replace(" ", "")}") } + }; + + + } + } } From 02b005dd9e26ba4ee8e5d5f4550fc13d81ff069c Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 14 Mar 2024 16:39:14 +0000 Subject: [PATCH 10/17] fix typos and update framework --- .../Properties/launchSettings.json | 2 +- .../Extensions/BookedOrderItemHelper.cs | 2 +- .../Feeds/FacilitiesFeeds.cs | 46 ++++--------------- .../Feeds/SessionsFeeds.cs | 44 ++---------------- 4 files changed, 14 insertions(+), 80 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json index bcd3c51f..b6b622f3 100644 --- a/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json +++ b/Examples/BookingSystem.AspNetCore/Properties/launchSettings.json @@ -6,7 +6,7 @@ "profiles": { "BookingSystem.AspNetCore": { "commandName": "Project", - "launchUrl": "https://localhost:5002/openactive", + "launchUrl": "https://localhost:5001/openactive", "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", diff --git a/Examples/BookingSystem.AspNetFramework/Extensions/BookedOrderItemHelper.cs b/Examples/BookingSystem.AspNetFramework/Extensions/BookedOrderItemHelper.cs index 5afcd484..997599fd 100644 --- a/Examples/BookingSystem.AspNetFramework/Extensions/BookedOrderItemHelper.cs +++ b/Examples/BookingSystem.AspNetFramework/Extensions/BookedOrderItemHelper.cs @@ -33,7 +33,7 @@ public static void AddPropertiesToBookedOrderItem(IOrderItemContext ctx, BookedO new PropertyValue() { Name = "Pin Code", - Description = bookedOrderItemInfo.PinCode, + Description = bookedOrderItemInfo.PinCode } }; ctx.ResponseOrderItem.AccessPass = new List diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs index 69cb9c4b..9bf1b691 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/FacilitiesFeeds.cs @@ -47,7 +47,7 @@ protected override async Task>> GetRpdeItems(long? af .SelectMulti(q) .Select(result => { - var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; + var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; var isGoldenRecord = faker.Random.Bool(); return new RpdeItem @@ -69,7 +69,12 @@ protected override async Task>> GetRpdeItems(long? af Identifier = result.Item1.Id, Name = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Name, Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), - Provider = GenerateOrganizer(result.Item2), + Provider = FeedGenerationHelper.GenerateOrganization( + faker, + result.Item2, + _appSettings.FeatureFlags.SingleSeller, + _appSettings.FeatureFlags.SingleSeller ? RenderSingleSellerId() : RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }) + ), Url = new Uri($"https://www.example.com/facilities/{result.Item1.Id}"), AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), @@ -165,41 +170,6 @@ protected override async Task>> GetRpdeItems(long? af } - private Organization GenerateOrganizer(SellerTable seller) - { - return _appSettings.FeatureFlags.SingleSeller ? new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - } : new Organization - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), - Name = seller.Name, - TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - }; - } - private List GenerateCategory(Faker faker, bool isGoldenRecord) { var listOfPossibleCategories = new List @@ -253,7 +223,7 @@ protected override async Task>> GetRpdeItems(long? afterTime .Take(RpdePageSize) .Select(x => { - var faker = new Faker() { Random = new Randomizer(((int)x.Modified + (int)x.Id)) }; + var faker = new Faker() { Random = new Randomizer((int)x.Id) }; return new RpdeItem { Kind = _appSettings.FeatureFlags.FacilityUseHasSlots ? RpdeKind.FacilityUseSlot : RpdeKind.IndividualFacilityUseSlot, diff --git a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs index 6a6a6595..3ab86dea 100644 --- a/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetFramework/Feeds/SessionsFeeds.cs @@ -107,7 +107,7 @@ protected override async Task>> GetRpdeItems(long? { var intt = (int)result.Item1.Modified; - var faker = new Faker() { Random = new Randomizer(((int)result.Item1.Modified + (int)result.Item1.Id)) }; + var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; // here we randomly decide whether the item is going to be a golden record or not by using Faker // See the README for more detail on golden records. var isGoldenRecord = faker.Random.Bool(); @@ -277,25 +277,6 @@ private QuantitativeValue GenerateAgeRange(Faker faker, bool isGoldenRecord) private ILegalEntity GenerateOrganizerOrPerson(Faker faker, SellerTable seller) { - if (_appSettings.FeatureFlags.SingleSeller) - return new Organization - { - Id = RenderSingleSellerId(), - Name = "Test Seller", - TaxMode = TaxMode.TaxGross, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - Telephone = faker.Phone.PhoneNumber("0#### ######"), - SameAs = new List { new Uri("https://socialmedia/testseller") } - }; if (seller.IsIndividual) return new OpenActive.NET.Person { @@ -305,25 +286,8 @@ private ILegalEntity GenerateOrganizerOrPerson(Faker faker, SellerTable seller) IsOpenBookingAllowed = true, Telephone = faker.Phone.PhoneNumber("07### ######") }; - return new Organization - { - Id = RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }), - Name = seller.Name, - TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet, - TermsOfService = new List - { - new PrivacyPolicy - { - Name = "Privacy Policy", - Url = new Uri("https://example.com/privacy.html"), - RequiresExplicitConsent = false - } - }, - IsOpenBookingAllowed = true, - Url = new Uri(faker.Internet.Url()), - Telephone = faker.Phone.PhoneNumber("0#### ######"), - SameAs = new List { new Uri($"https://socialmedia/{seller.Name}") } - }; + var organizationId = _appSettings.FeatureFlags.SingleSeller ? RenderSingleSellerId() : RenderSellerId(new SimpleIdComponents { IdLong = seller.Id }); + return FeedGenerationHelper.GenerateOrganization(faker, seller, _appSettings.FeatureFlags.SingleSeller, organizationId); } private List GenerateCategory(Faker faker, bool isGoldenRecord) @@ -367,7 +331,7 @@ static OpenActive.NET.Person GeneratePerson(Faker faker, bool isGoldenRecord) } var output = new List(); - var max = isGoldenRecord ? possibleMax : faker.Random.Number(possibleMax); + var max = isGoldenRecord ? possibleMax : faker.Random.Number(1, possibleMax); for (var i = 0; i < max; i++) { output.Add(GeneratePerson(faker, isGoldenRecord)); From f413b6d6f53fd147b4a6ebdcaa038c995ec16c8b Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Tue, 19 Mar 2024 15:06:53 +0000 Subject: [PATCH 11/17] add IS_CI env var and make minimal versions of rpde items --- .github/workflows/openactive-test-suite.yml | 1 + .../Feeds/FacilitiesFeeds.cs | 41 ++++++------- .../Feeds/SessionsFeeds.cs | 60 +++++++++++-------- .../Settings/AppSettings.cs | 1 + Examples/BookingSystem.AspNetCore/Startup.cs | 12 +++- .../FakeBookingSystem.cs | 2 +- 6 files changed, 65 insertions(+), 52 deletions(-) diff --git a/.github/workflows/openactive-test-suite.yml b/.github/workflows/openactive-test-suite.yml index 891d1bc2..b326d936 100644 --- a/.github/workflows/openactive-test-suite.yml +++ b/.github/workflows/openactive-test-suite.yml @@ -84,6 +84,7 @@ jobs: dotnet run --no-launch-profile --project ./server/Examples/BookingSystem.AspNetCore/BookingSystem.AspNetCore.csproj --configuration Release --no-build & env: ASPNETCORE_ENVIRONMENT: ${{ matrix.profile }} + IS_CI: true - name: Install OpenActive Test Suite run: npm install working-directory: tests diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 9bf1b691..194a340a 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -49,8 +49,7 @@ protected override async Task>> GetRpdeItems(long? af { var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; var isGoldenRecord = faker.Random.Bool(); - - return new RpdeItem + var facilityUseRpdeItem = new RpdeItem { Kind = RpdeKind.FacilityUse, Id = result.Item1.Id, @@ -68,22 +67,14 @@ protected override async Task>> GetRpdeItems(long? af }), Identifier = result.Item1.Id, Name = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Name, - Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), Provider = FeedGenerationHelper.GenerateOrganization( faker, result.Item2, _appSettings.FeatureFlags.SingleSeller, _appSettings.FeatureFlags.SingleSeller ? RenderSingleSellerId() : RenderSellerId(new SimpleIdComponents { IdLong = result.Item2.Id }) ), - Url = new Uri($"https://www.example.com/facilities/{result.Item1.Id}"), - AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), - AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), - AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)), - IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), - Category = GenerateCategory(faker, isGoldenRecord), - Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord), - Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null, Location = FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), + Url = new Uri($"https://www.example.com/facilities/{result.Item1.Id}"), FacilityType = GetNameAndFacilityTypeForFacility(result.Item1.Name, isGoldenRecord).Facility, IndividualFacilityUse = result.Item1.IndividualFacilityUses != null ? result.Item1.IndividualFacilityUses.Select(ifu => new OpenActive.NET.IndividualFacilityUse { @@ -97,6 +88,21 @@ protected override async Task>> GetRpdeItems(long? af }).ToList() : null, } }; + + var isCI = _appSettings.FeatureFlags.IsCI; + if (!isCI) + { + facilityUseRpdeItem.Data.Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)); + facilityUseRpdeItem.Data.AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord); + facilityUseRpdeItem.Data.AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord); + facilityUseRpdeItem.Data.AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)); + facilityUseRpdeItem.Data.IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }); + facilityUseRpdeItem.Data.Category = GenerateCategory(faker, isGoldenRecord); + facilityUseRpdeItem.Data.Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord); + facilityUseRpdeItem.Data.Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null; + } + + return facilityUseRpdeItem; }); return query.ToList(); @@ -181,19 +187,6 @@ private List GenerateCategory(Faker faker, bool isGoldenRecord) return FeedGenerationHelper.GetRandomElementsOf(faker, listOfPossibleCategories, isGoldenRecord, 1).ToList(); } - private List GenerateOpeningHours(Faker faker) - { - return new List - { - new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Sunday }, Opens = $"{faker.Random.Number(9,12)}:00", Closes = $"{faker.Random.Number(15,17)}:30"}, - new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Monday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, - new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Tuesday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, - new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Wednesday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, - new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Thursday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, - new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Friday }, Opens = $"{faker.Random.Number(6,10)}:00", Closes = $"{faker.Random.Number(18,21)}:30"}, - new OpeningHoursSpecification {DayOfWeek = new List {Schema.NET.DayOfWeek.Saturday }, Opens = $"{faker.Random.Number(9,12)}:00", Closes = $"{faker.Random.Number(15,17)}:30"} - }; - } } public class AcmeFacilityUseSlotRpdeGenerator : RpdeFeedModifiedTimestampAndIdLong diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index 3ab86dea..30e0b093 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -105,14 +105,14 @@ protected override async Task>> GetRpdeItems(long? .SelectMulti(q) .Select(result => { - var intt = (int)result.Item1.Modified; var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; // here we randomly decide whether the item is going to be a golden record or not by using Faker // See the README for more detail on golden records. var isGoldenRecord = faker.Random.Bool(); + var isCI = _appSettings.FeatureFlags.IsCI; - return new RpdeItem + var sessionSeriesRpdeItem = new RpdeItem { Kind = RpdeKind.SessionSeries, Id = result.Item1.Id, @@ -131,37 +131,47 @@ protected override async Task>> GetRpdeItems(long? Identifier = result.Item1.Id, Name = GetNameAndActivityForSessions(result.Item1.Title, isGoldenRecord).Name, EventAttendanceMode = MapAttendanceMode(result.Item1.AttendanceMode), - Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)), - AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord), - GenderRestriction = faker.Random.Enum(), - AgeRange = GenerateAgeRange(faker, isGoldenRecord), - Level = faker.Random.ListItems(new List { "Beginner", "Intermediate", "Advanced" }, 1).ToList(), Organizer = GenerateOrganizerOrPerson(faker, result.Item2), - AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord), - AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)), - IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), - Category = GenerateCategory(faker, isGoldenRecord), - Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord), - Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null, - Leader = GenerateListOfPersons(faker, isGoldenRecord, 2), - Contributor = GenerateListOfPersons(faker, isGoldenRecord, 2), - IsCoached = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }), Offers = GenerateOffers(faker, isGoldenRecord, result.Item1), // location MUST not be provided for fully virtual sessions Location = result.Item1.AttendanceMode == AttendanceMode.Online ? null : FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), - // beta:affiliatedLocation MAY be provided for fully virtual sessions - AffiliatedLocation = (result.Item1.AttendanceMode == AttendanceMode.Offline && faker.Random.Bool()) ? null : FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId), - EventSchedule = GenerateSchedules(faker, isGoldenRecord), - SchedulingNote = GenerateSchedulingNote(faker, isGoldenRecord), - IsAccessibleForFree = result.Item1.Price == 0, Url = new Uri($"https://www.example.com/sessions/{result.Item1.Id}"), Activity = GetNameAndActivityForSessions(result.Item1.Title, isGoldenRecord).Activity, - Programme = GenerateBrand(faker, isGoldenRecord), - IsInteractivityPreferred = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })), - IsVirtuallyCoached = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })), - ParticipantSuppliedEquipment = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? OpenActive.NET.RequiredStatusType.Optional : faker.Random.ListItem(new List { OpenActive.NET.RequiredStatusType.Optional, OpenActive.NET.RequiredStatusType.Required, OpenActive.NET.RequiredStatusType.Unavailable, null })), } }; + + // If this instance of the Reference Implementation is not for a CI run, then generate a comprehensive data. + // If it is for a CI run, return only the minimal properties needed + if (!isCI) + { + sessionSeriesRpdeItem.Data.Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)); + sessionSeriesRpdeItem.Data.AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.GenderRestriction = faker.Random.Enum(); + sessionSeriesRpdeItem.Data.AccessibilitySupport = FeedGenerationHelper.GenerateAccessibilitySupport(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.AccessibilityInformation = faker.Lorem.Paragraphs(isGoldenRecord ? 2 : faker.Random.Number(2)); + sessionSeriesRpdeItem.Data.IsWheelchairAccessible = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }); + sessionSeriesRpdeItem.Data.Category = GenerateCategory(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.Video = isGoldenRecord || faker.Random.Bool() ? new List { new VideoObject { Url = new Uri("https://www.youtube.com/watch?v=xvDZZLqlc-0") } } : null; + sessionSeriesRpdeItem.Data.Leader = GenerateListOfPersons(faker, isGoldenRecord, 2); + sessionSeriesRpdeItem.Data.Contributor = GenerateListOfPersons(faker, isGoldenRecord, 2); + sessionSeriesRpdeItem.Data.AgeRange = GenerateAgeRange(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.Image = FeedGenerationHelper.GenerateImages(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.Level = faker.Random.ListItems(new List { "Beginner", "Intermediate", "Advanced" }, 1).ToList(); + sessionSeriesRpdeItem.Data.IsCoached = isGoldenRecord || faker.Random.Bool() ? faker.Random.Bool() : faker.Random.ListItem(new List { true, false, null, null }); + // beta:affiliatedLocation MAY be provided for fully virtual sessions + sessionSeriesRpdeItem.Data.AffiliatedLocation = (result.Item1.AttendanceMode == AttendanceMode.Offline && faker.Random.Bool()) ? null : FeedGenerationHelper.GetPlaceById(result.Item1.PlaceId); + sessionSeriesRpdeItem.Data.EventSchedule = GenerateSchedules(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.SchedulingNote = GenerateSchedulingNote(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.IsAccessibleForFree = result.Item1.Price == 0; + sessionSeriesRpdeItem.Data.Programme = GenerateBrand(faker, isGoldenRecord); + sessionSeriesRpdeItem.Data.IsInteractivityPreferred = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })); + sessionSeriesRpdeItem.Data.IsVirtuallyCoached = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? true : faker.Random.ListItem(new List { true, false, null })); + sessionSeriesRpdeItem.Data.ParticipantSuppliedEquipment = result.Item1.AttendanceMode == AttendanceMode.Offline ? null : (isGoldenRecord ? OpenActive.NET.RequiredStatusType.Optional : faker.Random.ListItem(new List { OpenActive.NET.RequiredStatusType.Optional, OpenActive.NET.RequiredStatusType.Required, OpenActive.NET.RequiredStatusType.Unavailable, null })); + + } + + + return sessionSeriesRpdeItem; }); return query.ToList(); diff --git a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs index aa120720..eaf9ed38 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs @@ -20,6 +20,7 @@ public class FeatureSettings public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; public bool FacilityUseHasSlots { get; set; } = false; + public bool IsCI { get; set; } = false; } public class PaymentSettings diff --git a/Examples/BookingSystem.AspNetCore/Startup.cs b/Examples/BookingSystem.AspNetCore/Startup.cs index fb4b61b8..25bf1efa 100644 --- a/Examples/BookingSystem.AspNetCore/Startup.cs +++ b/Examples/BookingSystem.AspNetCore/Startup.cs @@ -20,14 +20,22 @@ public Startup(IConfiguration configuration) configuration.Bind(AppSettings); // Provide a simple way to disable token auth for some testing scenarios - if (System.Environment.GetEnvironmentVariable("DISABLE_TOKEN_AUTH") == "true") { + if (System.Environment.GetEnvironmentVariable("DISABLE_TOKEN_AUTH") == "true") + { AppSettings.FeatureFlags.EnableTokenAuth = false; } // Provide a simple way to enable FacilityUseHasSlots for some testing scenarios - if (System.Environment.GetEnvironmentVariable("FACILITY_USE_HAS_SLOTS") == "true") { + if (System.Environment.GetEnvironmentVariable("FACILITY_USE_HAS_SLOTS") == "true") + { AppSettings.FeatureFlags.FacilityUseHasSlots = true; } + + // Provide a simple way to enable CI mode + if (System.Environment.GetEnvironmentVariable("IS_CI") == "true") + { + AppSettings.FeatureFlags.IsCI = true; + } } public AppSettings AppSettings { get; } diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index ed8e3bbd..f34454dc 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -162,7 +162,7 @@ static FakeDatabase() } private static readonly int OpportunityCount = - int.TryParse(Environment.GetEnvironmentVariable("OPPORTUNITY_COUNT"), out var opportunityCount) ? opportunityCount : 2000; + int.TryParse(Environment.GetEnvironmentVariable("OPPORTUNITY_COUNT"), out var opportunityCount) ? opportunityCount : 20; /// /// TODO: Call this on a schedule from both .NET Core and .NET Framework reference implementations From ead80b249a98aa45c72b71fa80b79940e96fc4f0 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Tue, 19 Mar 2024 15:13:32 +0000 Subject: [PATCH 12/17] revert opportunity count --- Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs index f34454dc..ed8e3bbd 100644 --- a/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs +++ b/Fakes/OpenActive.FakeDatabase.NET/FakeBookingSystem.cs @@ -162,7 +162,7 @@ static FakeDatabase() } private static readonly int OpportunityCount = - int.TryParse(Environment.GetEnvironmentVariable("OPPORTUNITY_COUNT"), out var opportunityCount) ? opportunityCount : 20; + int.TryParse(Environment.GetEnvironmentVariable("OPPORTUNITY_COUNT"), out var opportunityCount) ? opportunityCount : 2000; /// /// TODO: Call this on a schedule from both .NET Core and .NET Framework reference implementations From f6740c94cce07f76263653a969724eb18308263a Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Tue, 19 Mar 2024 17:59:24 +0000 Subject: [PATCH 13/17] invert IS_CI to IS_LOREM_FITSUM_MODE --- .github/workflows/openactive-test-suite.yml | 1 - .../BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs | 6 ++++-- Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs | 9 +++++---- Examples/BookingSystem.AspNetCore/README.md | 9 +++++++-- .../BookingSystem.AspNetCore/Settings/AppSettings.cs | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/openactive-test-suite.yml b/.github/workflows/openactive-test-suite.yml index b326d936..891d1bc2 100644 --- a/.github/workflows/openactive-test-suite.yml +++ b/.github/workflows/openactive-test-suite.yml @@ -84,7 +84,6 @@ jobs: dotnet run --no-launch-profile --project ./server/Examples/BookingSystem.AspNetCore/BookingSystem.AspNetCore.csproj --configuration Release --no-build & env: ASPNETCORE_ENVIRONMENT: ${{ matrix.profile }} - IS_CI: true - name: Install OpenActive Test Suite run: npm install working-directory: tests diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 194a340a..182e3784 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -89,8 +89,10 @@ protected override async Task>> GetRpdeItems(long? af } }; - var isCI = _appSettings.FeatureFlags.IsCI; - if (!isCI) + // If this instance of the Reference Implementation is in Lorem Fitsum mode, then generate a comprehensive data. + // If it is not (eg for a CI run), return only the minimal properties needed + var IsLoremFitsumMode = _appSettings.FeatureFlags.IsLoremFitsumMode; + if (IsLoremFitsumMode) { facilityUseRpdeItem.Data.Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)); facilityUseRpdeItem.Data.AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord); diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index 30e0b093..9d1b1eab 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -110,7 +110,7 @@ protected override async Task>> GetRpdeItems(long? // here we randomly decide whether the item is going to be a golden record or not by using Faker // See the README for more detail on golden records. var isGoldenRecord = faker.Random.Bool(); - var isCI = _appSettings.FeatureFlags.IsCI; + var sessionSeriesRpdeItem = new RpdeItem { @@ -140,9 +140,10 @@ protected override async Task>> GetRpdeItems(long? } }; - // If this instance of the Reference Implementation is not for a CI run, then generate a comprehensive data. - // If it is for a CI run, return only the minimal properties needed - if (!isCI) + // If this instance of the Reference Implementation is in Lorem Fitsum mode, then generate a comprehensive data. + // If it is not (eg for a CI run), return only the minimal properties needed + var IsLoremFitsumMode = _appSettings.FeatureFlags.IsLoremFitsumMode; + if (IsLoremFitsumMode) { sessionSeriesRpdeItem.Data.Description = faker.Lorem.Paragraphs(isGoldenRecord ? 4 : faker.Random.Number(4)); sessionSeriesRpdeItem.Data.AttendeeInstructions = FeedGenerationHelper.GenerateAttendeeInstructions(faker, isGoldenRecord); diff --git a/Examples/BookingSystem.AspNetCore/README.md b/Examples/BookingSystem.AspNetCore/README.md index 49eb400b..f7b171a7 100644 --- a/Examples/BookingSystem.AspNetCore/README.md +++ b/Examples/BookingSystem.AspNetCore/README.md @@ -36,6 +36,11 @@ Due to this split of functionality, the sample data in the feeds are created/tra or not. For example, `Price` is important to booking and there is generated in FakeBookingSystem at startup and stored in the in-memory database. However `Terms Of Service` is not needed for booking, and therefore is generated at request time. -### Golden Records -Golden records are randomly generated records that contain all possible fields specified by the OpenActive Modelling Specification. +### Lorem Fitsum mode +When Reference Implementation is run in Lorem Fitsum mode, the data generated contains all the possible fields specified by the OpenActive Modelling Specification. They are unrealistic representations of data, and the presence of all the fields should not be relied on when developing front-end representations of the data. +However it is very useful for data consumers and deciding on how to present the data to the users. + +### Golden Records +Golden records are randomly generated records that have maximally enriched properties in the generated data. For example where a record might have one image normally, a golden record will have four. + diff --git a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs index eaf9ed38..07edbb7c 100644 --- a/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs +++ b/Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs @@ -20,7 +20,7 @@ public class FeatureSettings public bool OnlyFreeOpportunities { get; set; } = false; public bool PrepaymentAlwaysRequired { get; set; } = false; public bool FacilityUseHasSlots { get; set; } = false; - public bool IsCI { get; set; } = false; + public bool IsLoremFitsumMode { get; set; } = false; } public class PaymentSettings From 416b9cf5b7bed2084cc937e2c0b177720bf485a0 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 20 Mar 2024 14:41:57 +0000 Subject: [PATCH 14/17] fix builf --- Examples/BookingSystem.AspNetCore/Startup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Startup.cs b/Examples/BookingSystem.AspNetCore/Startup.cs index 25bf1efa..c02b7b91 100644 --- a/Examples/BookingSystem.AspNetCore/Startup.cs +++ b/Examples/BookingSystem.AspNetCore/Startup.cs @@ -32,9 +32,9 @@ public Startup(IConfiguration configuration) } // Provide a simple way to enable CI mode - if (System.Environment.GetEnvironmentVariable("IS_CI") == "true") + if (System.Environment.GetEnvironmentVariable("IS_LOREM_FITSUM_MODE") == "true") { - AppSettings.FeatureFlags.IsCI = true; + AppSettings.FeatureFlags.IsLoremFitsumMode = true; } } From d88cc7f57c48fab4232bfef6ca4388c3f021c078 Mon Sep 17 00:00:00 2001 From: civsiv Date: Wed, 27 Mar 2024 15:18:14 +0000 Subject: [PATCH 15/17] Update Examples/BookingSystem.AspNetCore/README.md Co-authored-by: Luke Winship --- Examples/BookingSystem.AspNetCore/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/BookingSystem.AspNetCore/README.md b/Examples/BookingSystem.AspNetCore/README.md index f7b171a7..0e851101 100644 --- a/Examples/BookingSystem.AspNetCore/README.md +++ b/Examples/BookingSystem.AspNetCore/README.md @@ -37,7 +37,7 @@ or not. For example, `Price` is important to booking and there is generated in F needed for booking, and therefore is generated at request time. ### Lorem Fitsum mode -When Reference Implementation is run in Lorem Fitsum mode, the data generated contains all the possible fields specified by the OpenActive Modelling Specification. +When Reference Implementation is run in Lorem Fitsum (a play on [Lorem Ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum)) mode, the data generated contains all the possible fields specified by the OpenActive Modelling Specification. They are unrealistic representations of data, and the presence of all the fields should not be relied on when developing front-end representations of the data. However it is very useful for data consumers and deciding on how to present the data to the users. From e831d3a491d80ffb721b5d8a76aaa6880bd31987 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 27 Mar 2024 15:37:07 +0000 Subject: [PATCH 16/17] add some fixes to readme --- Examples/BookingSystem.AspNetCore/README.md | 32 +++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/README.md b/Examples/BookingSystem.AspNetCore/README.md index 0e851101..f8762cf3 100644 --- a/Examples/BookingSystem.AspNetCore/README.md +++ b/Examples/BookingSystem.AspNetCore/README.md @@ -2,22 +2,36 @@ An example OpenActive.Server.NET implementation. -This implementation is also used as a reference implementation for the [Test Suite](https://github.com/openactive/openactive-test-suite) to run its tests against. +This implementation is also used as a Reference Implementation for the [Test Suite](https://github.com/openactive/openactive-test-suite) to run its tests against. -## Running Locally +## Running Locally using Visual Studio -1. In Visual Studio, run the BookingSystem.AspNetCore project +In Visual Studio, run the BookingSystem.AspNetCore project - When it's finished building, it will open a page in your browser with a randomly assigned port e.g. http://localhost:55603/. Make note of this port. +When it's finished building, it will open a page in your browser on port 5001. - Head to `http://localhost:{PORT}/openactive` to check that the project is running correctly. You should see an Open Data landing page. -2. Head to BookingSystem.AspNetCore project options and add an env var using the port you made note of earlier: - - `ApplicationHostBaseUrl: http://localhost:{PORT}` -3. Now, re-run the project. You're good to go 👍 +Head to `http://localhost:5001/openactive` to check that the project is running correctly. You should see an Open Data landing page. See the [project contribution documentation](/CONTRIBUTING.md) for details on how to run BookingSystem.AspNetCore locally. +## Running Locally using the CLI + +Open a terminal in `Examples/BookingSystem.AspNetCore` directory + +Run: + +```sh +dotnet run +``` + +If you want to start the Reference Implementation in a specific environment run the following: + +```sh +ASPNETCORE_ENVIRONMENT=no-auth dotnet run --no-launch-profile --project ./BookingSystem.AspNetCore.csproj --configuration Release --no-build +``` + +The above example starts the Reference Implementation in `no-auth` mode. + ## Reference Implementation Data Generation Reference Implementation has three main uses that make it very important in the OpenActive ecosystem: From 729c67a977969f33fc135d2feefdc50d442f9ed7 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Wed, 27 Mar 2024 16:14:20 +0000 Subject: [PATCH 17/17] review changes --- .../Feeds/FacilitiesFeeds.cs | 2 +- .../Feeds/SessionsFeeds.cs | 2 +- Examples/BookingSystem.AspNetCore/README.md | 21 +++++++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs index 182e3784..4ea35f33 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs @@ -48,7 +48,7 @@ protected override async Task>> GetRpdeItems(long? af .Select(result => { var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; - var isGoldenRecord = faker.Random.Bool(); + var isGoldenRecord = faker.Random.Number(0, 1) > 0.75; var facilityUseRpdeItem = new RpdeItem { Kind = RpdeKind.FacilityUse, diff --git a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs index 9d1b1eab..302aa382 100644 --- a/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs +++ b/Examples/BookingSystem.AspNetCore/Feeds/SessionsFeeds.cs @@ -109,7 +109,7 @@ protected override async Task>> GetRpdeItems(long? var faker = new Faker() { Random = new Randomizer((int)result.Item1.Id) }; // here we randomly decide whether the item is going to be a golden record or not by using Faker // See the README for more detail on golden records. - var isGoldenRecord = faker.Random.Bool(); + var isGoldenRecord = faker.Random.Number(0, 1) > 0.75; var sessionSeriesRpdeItem = new RpdeItem diff --git a/Examples/BookingSystem.AspNetCore/README.md b/Examples/BookingSystem.AspNetCore/README.md index f8762cf3..38bd0a32 100644 --- a/Examples/BookingSystem.AspNetCore/README.md +++ b/Examples/BookingSystem.AspNetCore/README.md @@ -2,7 +2,8 @@ An example OpenActive.Server.NET implementation. -This implementation is also used as a Reference Implementation for the [Test Suite](https://github.com/openactive/openactive-test-suite) to run its tests against. +This implementation is also used as a reference implementation for the [Test Suite](https://github.com/openactive/openactive-test-suite) to run its tests against and therefore is often to as Reference Implementation. +Until there are more reference implementations, all references to Reference Implementation refer to this implementation and Reference Implementation and BookingSystem.AspNetCore can be used interchangeably. ## Running Locally using Visual Studio @@ -24,17 +25,17 @@ Run: dotnet run ``` -If you want to start the Reference Implementation in a specific environment run the following: +If you want to start BookingSystem.AspNetCore in a specific environment run the following: ```sh ASPNETCORE_ENVIRONMENT=no-auth dotnet run --no-launch-profile --project ./BookingSystem.AspNetCore.csproj --configuration Release --no-build ``` -The above example starts the Reference Implementation in `no-auth` mode. +The above example starts the BookingSystem.AspNetCore in `no-auth` mode. -## Reference Implementation Data Generation +## BookingSystem.AspNetCore Data Generation -Reference Implementation has three main uses that make it very important in the OpenActive ecosystem: +BookingSystem.AspNetCore has three main uses that make it very important in the OpenActive ecosystem: - For data publishers / booking systems: It is used to demonstrate the properties and shape of data and APIs, according to the OpenActive specifications - For data users / brokers: It is used as a trial integration where testing can be done with no ramifications - For contributors: It is used to ensure the Test Suite tests are correct and passing, for different combinations of Open Booking API features. @@ -51,10 +52,18 @@ or not. For example, `Price` is important to booking and there is generated in F needed for booking, and therefore is generated at request time. ### Lorem Fitsum mode -When Reference Implementation is run in Lorem Fitsum (a play on [Lorem Ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum)) mode, the data generated contains all the possible fields specified by the OpenActive Modelling Specification. +When BookingSystem.AspNetCore is run in Lorem Fitsum (a play on [Lorem Ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum)) mode, the data generated contains all the possible fields specified by the OpenActive Modelling Specification. They are unrealistic representations of data, and the presence of all the fields should not be relied on when developing front-end representations of the data. However it is very useful for data consumers and deciding on how to present the data to the users. +Lorem Fitsum mode can be running by setting the environment variable `IS_LOREM_FITSUM_MODE` to `true`. +In Visual Studio this can be done in Properties > BookingSystem.AspNetCore Properties > Run > Default > Environment Variables. +In the CLI this can be done by running the following command for example: + +```sh +IS_LOREM_FITSUM_MODE=true dotnet run --no-launch-profile --project ./BookingSystem.AspNetCore.csproj --configuration Release --no-build +``` + ### Golden Records Golden records are randomly generated records that have maximally enriched properties in the generated data. For example where a record might have one image normally, a golden record will have four.