Day 4: C1 and C2 without leases

Objective for Day 4

Implement C1 and C2 responses without leases.

Rationale

The C1, C2 and B responses contain opportunity data that describes the bookable opportunities. This step focusses on just returning valid opportunity data GetOrderItem and completing the other configuration required for C1 and C2 to return responses as expected.

Step 1: Set up your Order ID structure

The Order IDs are configured similarly to SellerIdComponents. Within BookingEngineSettings the OrderIdTemplate setting controls how the Order ID is serialised and deserialised, using the base URL defined in OrderBaseUrl.

Note that unlike other JSON-LD IDs, the Order ID must be resolvable, and so must match the endpoint bindings configured in the controller. Hence OrderBaseUrl used for Order IDs instead of JsonLdIdBaseUrl.

There is a built-in POCO named OrderIdComponents that is defined as follows:

public class OrderIdComponents
{
    public OrderType? OrderType { get; set; }
    public string ClientId { get; set; }
    public string uuid { get; set; }
    public long? OrderItemIdLong { get; set; }
    public string OrderItemIdString { get; set; }
}

The uuid must be included in the OrderIdTemplate, as must the OrderType (which is resolved to either "order-quotes" or "orders").

Depending on the type of your internal Order Item ID, you may use either OrderItemIdLong or OrderItemIdString within the URL template. Once you have chosen which one to use, simply reference that same property consistently wherever you use the Order Item ID throughout your code, and ignore the other property.

Note that the ClientId is reserved for use for authentication (see Day 8), and must not be included in the Order ID.

The following example demonstrates BookingSystemSettings to configure Order IDs:

OrderBaseUrl = new Uri(baseUrl + "api/openbooking/"),
OrderIdTemplate = new OrderIdTemplate(
    "{+BaseUrl}{OrderType}/{uuid}",
    "{+BaseUrl}{OrderType}/{uuid}#/orderedItems/{OrderItemIdLong}"
    ),

Step 2: GetOrderItem

This step will implement the GetOrderItem within the implementations of OpportunityStore created in Day 3.

protected override void GetOrderItem(List<OrderItemContext<SessionOpportunity>> orderItemContexts, StoreBookingFlowContext flowContext)
{
    ...
}

The objective of GetOrderItem is to create a response OrderItem for each request OrderItem provided.

The OpportunityStore methods generally deal with lists of OrderItemContext, where each contains a OrderItem request object, along with methods to add an OrderItem response object. This allows your implementation to process each OrderItem individually, and also to optimise database calls where groups of OrderItem (if they are handled by the same OpportunityStore) can be retrieved together.

OrderItemContext capabilities

Each OrderItemContext contains the following properties that represent the request:

  • RequestBookableOpportunityOfferId - The ID of the OrderItem, to be looked up in your database

  • RequestOrderItem - The OrderItem from the original request

Each OrderItemContext also contains the following methods, which can be used to construct a response:

  • SetResponseOrderItem - Set the provided OrderItem as a response, and checks that the Seller provided matches that specified by context.SellerIdComponents.

  • SetResponseOrderItemAsSkeleton - Create a skeleton response OrderItem from the request OrderItem (useful for adding errors)

  • AddError - Adds an error to the response. Note this must be called after a response exists (i.e. after SetResponseOrderItem or SetResponseOrderItemAsSkeleton).

  • ValidateAttendeeDetails - Automatically validates attendee details in the request and sets errors on the response based on the response values of AttendeeDetailsRequired and OrderItemIntakeForm (it requires these response values to be set before this is called).

GetOrderItem implementation requirements

Your implementation of GetOrderItem must achieve the following behaviour:

  • For each OrderItemContext provided in the list, parse the request and set a response.

Note that for the common case of multi-party bookings the same RequestBookableOpportunityOfferId may be found across multiple OrderItemContext.

Additionally, it must handle the following error cases using AddError. Note that depending on the architecture of the booking system, some of these error conditions may be more reliably checked at LeaseOrderItems or BookOrderItems within the transaction (see Day 5).

Also note that capacity errors must take into account any opportunities that are leased by this current request's UUID (e.g. from previous calls to C1 and C2) and must exclude these from capacity calculations.

Error case

AddError(...)

Specified opportunity does not exist

UnknownOpportunityDetailsError

Specified offer does not exist within that opportunity

UnknownOfferError

Specified opportunity and offer do exist, but are not bookable for any reason

UnavailableOpportunityError

There are conflicts between OrderItems provided e.g. if multiple opportunities use the same physical space, and may not be booked together

OpportunityIsInConflictError

No spaces available for an opportunity at all

OpportunityIsFullError

Taking into account the number of OrderItemContext instances in the list that relate to the same opportunity, there is insufficient capacity to book this particular OrderItemContext.

As per the Open Booking API specification, this error must only be included against the OrderItemContext instances which are in excess of the capacity - for example in an Opportunity with a remainingAttendeeCapacity of 2 and with 5 OrderItemContext instances related to it, this error would only be emitted against 3 of the OrderItemContext instances.

OpportunityHasInsufficientCapacityError

Taking into account the number of OrderItemContext instances in the list that relate to the same opportunity, where is insufficient capacity to book this particular OrderItemContext due to a lease being held.

As per the Open Booking API specification, this error must only be included against the OrderItemContext instances which are in excess of the capacity specifically due to a lease being held (instead of the error OpportunityHasInsufficientCapacityError) - for example in an Opportunity with a remainingAttendeeCapacity of 3, with 1 additional space held by another lease, then where there are 9 OrderItemContext related to that same Opportunity, this error would only be emitted against 1 of the OrderItemContext instances, with OpportunityHasInsufficientCapacityError emitted against the other 5 of the OrderItemContext instances. This helps a Customer to know whether they should try again.

OpportunityCapacityIsReservedByLeaseError

And optionally validate attendee details provided:

  • Validate any attendeeDetails or orderItemIntakeFormResponse provided and use AddError to add an IncompleteAttendeeDetailsError, IncompleteIntakeFormError or InvalidIntakeFormError.

  • This can be achieved using ctx.ValidateDetails(flowContext.Stage), with additional validation logic executed afterwards as required.

  • This is only required if your booking system supports attendee details, otherwise simply ignore these properties in the request and they will therefore not appear in the response.

Helpers available

Note that the helpers available within the OpportunityDataRPDEFeedGenerator implementations (from Day 2) are also available here:

Method

Example

RenderOpportunityId

Id = this.RenderOpportunityId(new SessionOpportunity

{

OpportunityType = OpportunityType.SessionSeries,

SessionSeriesId = @class.Id

}),

RenderOfferId

Id = this.RenderOfferId(new SessionOpportunity

{

OfferOpportunityType = OpportunityType.SessionSeries,

SessionSeriesId = @class.Id,

OfferId = 0

}),

RenderSellerId

(for Multiple Sellers)

Id = this.RenderSellerId(new SellerIdComponents

{

SellerIdLong = seller.Id

}),

RenderSingleSellerId

(for Single Seller)

Id = this.RenderSingleSellerId(),

Step 3: Supported Fields

Customise the following StoreBookingEngineSettings within EngineConfig.cs to include only those fields that your booking system supports and stores (as only supported fields must be reflected back to the Broker):

  • CustomerPersonSupportedFields

  • CustomerOrganizationSupportedFields

  • BrokerSupportedFields

For example:

CustomerPersonSupportedFields = p => new Person {
    Email = p.Email,
    GivenName = p.GivenName,
    FamilyName = p.FamilyName,
    Telephone = p.Telephone
},

Step 4: Booking Service Details

Complete the details in the setting below to include information about your booking system:

  • BookingServiceDetails

For example:

BookingServiceDetails = new BookingService
{
    Name = "Acme booking system",
    Url = new Uri("https://example.com"),
    TermsOfService = new List<Terms>
    {
        new PrivacyPolicy
        {
            Url = new Uri("https://example.com/privacy.html")
        }
    }
},

Step 5: SellerStore

Implement a new SellerStore, with the method GetSeller that returns an Organization or Person object. This object must include at least Name and TaxMode properties.

If the Seller is not found, simply return null to generate the correct OpenBookingError.

Configure the SellerStore setting of BookingEngineSettings within EngineConfig.cs to use this new implementation of SellerStore:

SellerStore = new MyCustomSellerStore(),

Booking systems supporting Multiple Sellers

For a booking system supporting Multiple Sellers, your implementation of GetSeller must return an Organization or Person that represents the sellerIdComponents supplied, using either SellerIdLong or SellerIdString depending on your SellerIdTemplate.

The returned object must include an Id property containing a URL representing the sellerIdComponents supplied, which can be rendered using RenderSellerId(...).

The following example demonstrates GetSeller for Multiple Sellers:

protected override ILegalEntity GetSeller(SellerIdComponents sellerIdComponents)
{
    using (var db = FakeBookingSystem.Database.Mem.Database.Open())
    {
        var seller = db.SingleById<SellerTable>(sellerIdComponents.SellerIdLong);
        if (seller == null)
        {
            // Seller not found
            return null;
        }

        return new Organization
        {
            Id = RenderSellerId(new SellerIdComponents { SellerIdLong = seller.Id }),
            Name = seller.Name,
            TaxMode = seller.IsTaxGross ? TaxMode.TaxGross : TaxMode.TaxNet,
            LegalName = seller.Name,
            // Note these strings should be populated from the database
            Address = new PostalAddress
            {
                StreetAddress = "1 Hidden Gem",
                AddressLocality = "Another town",
                AddressRegion = "Oxfordshire",
                PostalCode = "OX1 1AA",
                AddressCountry = "GB"
            },
            TermsOfService = new List<Terms>
            {
                new PrivacyPolicy
                {
                    Name = "Privacy Policy",
                    Url = new Uri("https://example.com/privacy.html"),
                    RequiresExplicitConsent = false
                }
            },
            IsOpenBookingAllowed = true,
        };
    }
}

Booking systems supporting a Single Seller

For a Single Seller booking system, your implementation of GetSeller must return an Organization or Person that represents the Single Seller for which your booking system is configured.

In Single Seller mode both SellerIdLong and SellerIdString properties of sellerIdComponents will be null, and the data for your Seller should instead be retrieved from your booking system configuration table.

RenderSingleSellerId() is provided to render the Id.

The following example demonstrates BookingSystemSettings for a hard-coded Single Seller:

protected override ILegalEntity GetSeller(SellerIdComponents sellerIdComponents)
{
    // For Single Seller booking systems, no ID will be available from sellerIdComponents, and this data should instead come from your configuration table
    return new Organization
    {
        Id = RenderSingleSellerId(),
        Name = "Test Seller",
        TaxMode = TaxMode.TaxGross,
        LegalName = "Test Seller Ltd",
        Address = new PostalAddress
        {
            StreetAddress = "1 Hidden Gem",
            AddressLocality = "Another town",
            AddressRegion = "Oxfordshire",
            PostalCode = "OX1 1AA",
            AddressCountry = "GB"
        },
        TermsOfService = new List<Terms>
        {
            new PrivacyPolicy
            {
                Name = "Privacy Policy",
                Url = new Uri("https://example.com/privacy.html"),
                RequiresExplicitConsent = false
            }
        },
        IsOpenBookingAllowed = true,
    };
}

Step 6: Run Test Suite

The availability-check feature within the openactive-integration-tests test suite should pass.

Run this test in isolation as follows:

npm start -- --runInBand test/features/core/availability-check/

Last updated