openactive / open-booking-api

Repository for the Open Booking API specification
Other
2 stars 3 forks source link

Add new property for AddOns #224

Open nathansalter opened 2 years ago

nathansalter commented 2 years ago

Proposer

Playfinder/Bookteq

Use Case

When pulling a list of FacilityUses, I want to be able to view what additional paid facilities this has. Examples include racket hire, floodlight hire, tennis balls and other sports equipment.

Why is this not covered by existing properties?

An Offer is described as:

the terms under which a participant can pay to attend an event.

This is currently too restrictive to allow other types of Offers to be purchased by the Customer.

Please provide a link to example data

The Bookteq widget on Padel4all (https://play.padel4all.com/dashboard/book user account required) has options for hiring equipment.

image

Proposal

The FacilityUse and IndividualFacilityUse models should have an extra beta:addOns property which contains an array of Offer models

Example

"beta:addOns": [{
  "identifier": "https://padel4all.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
  "name": "Adult Racket Hire",
  "price": 1.5,
  "priceCurrency": "GBP",
  "priceSpecification": {
    "eligibleQuantity": {
      "@type": "QuantitiveValue",
      "minValue": 0,
      "maxValue": 4
    }
  }
}, {
  "identifier": "https://padel4all.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
  "name": "Junior Racket Hire",
  "price": 0,
  "priceCurrency": "GBP",
  "priceSpecification": {
    "eligibleQuantity": {
      "@type": "QuantitiveValue",
      "minValue": 0,
      "maxValue": 4
    }
  }
}]
nickevansuk commented 2 years ago

This is really great! A couple of modelling notes:

So for example, something like this perhaps:

"beta:addOn": [
  {
    "@type": "Product",
    "@id": "https://padel4all.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
    "name": "Adult Racket Hire",
    "beta:availableQuantity": {
      "@type": "QuantitiveValue",
      "minValue": 0,
      "maxValue": 4
    },
    "ageRange": {
      "@type": "QuantitativeValue",
      "minValue": 16
    },
    "offers": [
      {
        "@id": "https://padel4all.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#/offers/1",
        "@type": "Offer",
        "price": 1.5,
        "priceCurrency": "GBP"
      }
    ]
  }
]
nathansalter commented 2 years ago

Great suggestions, all makes a lot of sense! I thought we should get this sorted out in preparation before tackling the slightly more complicated booking system. I also think we should specify what to do in the case of free addons, for example if Junior racket hire was free but adult was not. Would it be reasonable just to have the offers.price set to 0 do you think?

nickevansuk commented 2 years ago

Great plan - and yes offers.price set to 0 is exactly how the booking spec handles free opportunities currently, so that seems consistent 👍

Might be worth thinking about booking at the same time in case it affects the model? If this was to be added to beta, would be good to ensure that it had that broader compatibility?

nathansalter commented 2 years ago

I'm thinking for adding to the booking OrderItem in this format:

  "orderedItem": [
    {
      "@type": "OrderItem",
      "orderedItem": "https://example.com/api/openactive/slots/12345",
      "acceptedOffer": "https://example.com/api/openactive/offers/12345",
      "beta:addOn": [{
        "@id": "https://example.com/api/openactive/add-on/4",
        "quantity": 3,
        "acceptedOffer": "https://example.com/api/openactive/add-on/4#/offers/1"
      },{
        "@id": "https://example.com/api/openactive/add-on/5",
        "quantity": 0,
        "acceptedOffer": "https://example.com/api/openactive/add-on/5#/offers/1"
      }]
      "position": 0
    },
    {
      "@type": "OrderItem",
      "orderedItem": "https://example.com/api/openactive/slots/23456",
      "acceptedOffer": "https://example.com/api/openactive/offers/23456",
      "beta:addOn": [{
        "@id": "https://example.com/api/openactive/add-on/4",
        "quantity": 1,
        "acceptedOffer": "https://example.com/api/openactive/add-on/4#/offers/1"
      },{
        "@id": "https://example.com/api/openactive/add-on/5",
        "quantity": 1,
        "acceptedOffer": "https://example.com/api/openactive/add-on/5#/offers/2"
      }],
      "position": 1
    }
  ],
nickevansuk commented 2 years ago

So they're dependents of the OrderItem?

If so I wonder if it might be worth thinking about the expected behaviour for e.g. cancellation and replacement - do the addOns get immediately cancelled along with their parent OrderItem?

Perhaps there's a more generic structure that can be used here so that interaction with OrderItems can be homogeneous, so something roughly like this:

"orderedItem": [
  {
    "@type": "OrderItem",
    "orderedItem": "https://example.com/api/openactive/slots/23456",
    "acceptedOffer": "https://example.com/api/openactive/offers/23456",
    "position": 0
  },
  {
    "@type": "OrderItem",
    "orderedItem": "https://example.com/api/openactive/add-on/4",
    "acceptedOffer": "https://example.com/api/openactive/add-on/4#/offers/1",
    "beta:addOnForPosition": 0,
    "position": 1
  },
  {
    "@type": "OrderItem",
    "orderedItem": "https://example.com/api/openactive/add-on/4",
    "acceptedOffer": "https://example.com/api/openactive/add-on/4#/offers/1",
    "beta:addOnForPosition": 0,
    "position": 2
  },
  {
    "@type": "OrderItem",
    "orderedItem": "https://example.com/api/openactive/add-on/5",
    "acceptedOffer": "https://example.com/api/openactive/add-on/5#/offers/2",
    "beta:addOnForPosition": 0,
    "position": 3
  }
]
nathansalter commented 2 years ago

I think it makes sense for them to be dependents of the OrderItem. For two main reasons, firstly if you're hiring equipment it makes sense for that to be at a specific time and duration. Secondly, if you're purchasing equipment it's important for the Venue to know when that particular item has to be in stock, and when you're expecting it. Consider the example of racket hire, if you're cancelling that opportunity it no longer makes sense to hire the rackets.

nathansalter commented 2 years ago

After further discussion it's agreed that they should be flat, as in Nick's example above. This is so that we can still handle the OrderItem cancellation/modification flows such as for standard OrderItems.

nickevansuk commented 2 years ago

Also do we also need a beta:addOnForId to sit alongside the beta:addOnForPosition for the response from P and B (positions don't exist after P in the approval flow, as they exist only within the lifetime of the request)?

In fact, looking at the modelling again, perhaps we should model this as a more generic beta:parentOrderItem with range OrderItem?

C1, C2, B request

{
  "@type": "OrderItem",
  "orderedItem": "https://example.com/api/openactive/add-on/4",
  "acceptedOffer": "https://example.com/api/openactive/add-on/4#/offers/1",
  "beta:parentOrderItem": {
    "@type": "OrderItem",
    "position": 1
  },
  "position": 4
},

C1, C2 response:

{
  "@type": "OrderItem",
  "orderedItem": {...},
  "acceptedOffer": {...},
  "beta:parentOrderItem": {
    "@type": "OrderItem",
    "position": 1
  },
  "position": 4
},

P response:

{
  "@type": "OrderItem",
  "orderedItem": {...},
  "acceptedOffer": {...},
  "beta:parentOrderItem": {
    "@type": "OrderItem",
    "@id": "https://example.com/api/openbooking/orders/63cc36f8-e755-4445-99b6-739ff03f3c77#/orderedItems/1994",
    "position": 1
  },
  "position": 4
},

At B response:

{
  "@type": "OrderItem",
  "orderedItem": {...},
  "acceptedOffer": {...},
  "beta:parentOrderItem": {
    "@type": "OrderItem",
    "@id": "https://example.com/api/openbooking/orders/63cc36f8-e755-4445-99b6-739ff03f3c77#/orderedItems/1994"
  }
},
nathansalter commented 2 years ago

Yes I think that's a sensible suggestion, and nicely abstracts it for future additions

nickevansuk commented 2 years ago

@nathansalter areas to address from our call just now:

Note when referencing items across Orders we'd probably need a different request and potentially more detail in the Orders feed to allow the Broker to dereference the Order (as the URI scheme of the OrderItem is known only to the booking system) e.g. as below:

Request:

{
  "@type": "OrderItem",
  "orderedItem": "https://example.com/api/openactive/add-on/4",
  "acceptedOffer": "https://example.com/api/openactive/add-on/4#/offers/1",
  "beta:parentOrderItem": {
    "@type": "OrderItem",
    "@id": "https://example.com/api/openbooking/orders/63cc36f8-e755-4445-99b6-739ff03f3c77#/orderedItems/1994"
  },
  "position": 4
},

Response / Orders Feed contents

{
  "@type": "OrderItem",
  "orderedItem": "https://example.com/api/openactive/add-on/4",
  "acceptedOffer": "https://example.com/api/openactive/add-on/4#/offers/1",
  "beta:parentOrderItem": {
    "@type": "OrderItem",
    "@id": "https://example.com/api/openbooking/orders/63cc36f8-e755-4445-99b6-739ff03f3c77#/orderedItems/1994"
    "beta:order": {
      "@type": "Order",
      "@id": "https://example.com/api/openbooking/orders/63cc36f8-e755-4445-99b6-739ff03f3c77"
      "identifier": "63cc36f8-e755-4445-99b6-739ff03f3c77"
    }
  },
  "position": 4
},
nickevansuk commented 2 years ago

I've also transferred this to Open Booking API, as the functionality around the modelling may be as important to reach consensus on as the modelling itself? It appears that the modelling exists here for the purposes of facilitating booking functionality rather than purely to publish open data?

nathansalter commented 2 years ago

Add Ons

AddOns in Availability feeds

Collating this all into a single issue, we have stage 1 with adding this into the IndividualFacilityUse feeds

{
  "next": "https://example.com/api/open-active/4813dbc0-41df-42c3-aa85-cff6f89531dd/individual-facility-uses?afterTimestamp=1643121185935915&afterId=2d21b9fb-ebb1-49b1-a07c-fdb81dba5c23",
  "items": [
    {
      "state": "updated",
      "kind": "IndividualFacilityUse",
      "id": "d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
      "modified": 1592994256385869,
      "data": {
        "@id": "https://example.com/api/open-active/fe30c0e1-b4f9-4226-9fd8-3ddb230edd9f/individual-facility-uses/d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
        "@type": "IndividualFacilityUse",
        "@context": [
          "https://openactive.io/",
          "https://openactive.io/ns-beta"
        ],
        "identifier": "d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
        "name": "Football 11-a-side",
        "url": "https://example.com/api/open-active/fe30c0e1-b4f9-4226-9fd8-3ddb230edd9f/individual-facility-uses/d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
        "activity": [
          {
            "@type": "Concept",
            "@id": "https://openactive.io/activity-list#117e7f70-6c42-4b1f-a3bb-620b63ea263l",
            "inScheme": "https://openactive.io/activity-list",
            "prefLabel": "11-a-side"
          }
        ],
        "beta:addOn": [{
          "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
          "@type": "AddOn",
          "name": "Adult Racket Hire",
          "priceSpecification": {
            "eligibleQuantity": {
              "@type": "QuantitiveValue",
              "minValue": 0,
              "maxValue": 4
            }
          },
          "offers": [
            {
              "@type": "Offer",
              "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
              "priceCurrency": "GBP",
              "price": 1.5
            }
          ]
        }, {
          "@id": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
          "@type": "AddOn",
          "name": "Junior Racket Hire",
          "priceSpecification": {
            "eligibleQuantity": {
              "@type": "QuantitiveValue",
              "minValue": 0,
              "maxValue": 4
            }
          },
          "offers": [
            {
              "@type": "Offer",
              "@id": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
              "priceCurrency": "GBP",
              "price": 0
            }
          ]
        }],
        "hoursAvailable": [
          ...
        ],
        "location": {
          ...
        },
        "provider": {
          "@type": "Organization",
          "@id": "https://example.com/api/venues/fe30c0e1-b4f9-4226-9fd8-3ddb230edd9f",
          "name": "Sundridge Park Lawn Tennis & Squash Rackets Club"
        }
      }
    }
  ],
  "license": "https://creativecommons.org/licenses/by/4.0/"
}

Objects in the beta:addOn property indicate that the facility can provide AddOns with the provided offers. Brokers SHOULD offer these at checkout for the customer to optionally select. The minimum eligibleQuantity MAY be above zero to indicate that this is a required AddOn. It is RECOMMENDED that when this is the case, brokers include this extra cost (if applicable) into the cost shown to the customer before slot selection takes place. The Broker MUST adhere to the minimum/maximum quantity values shown here when submitting Order requests at C1, C2, B etc.

AddOns in Booking Flow

And Stage 2 adding this into the booking APIs:

image

In the image above, it indicates how the structure of the order items is assumed to be. Although it's possible that you can have a deeper structure than a single AddOn, currently brokers SHOULD only use a single level of nesting to help implementation from the booking system side.

The standard booking api can be updated to the following:

C1 Request

PUT /api/order-quote-templates/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/1.1
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    "@type": "Organization",
    "name": "MyFitnessApp",
    "url": "https://myfitnessapp.example.com",
    "description": "A fitness app for all the community",
    "logo": {
      "@type": "ImageObject",
      "url": "http://data.myfitnessapp.org.uk/images/logo.png"
    },
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "Alan Peacock Way",
      "addressLocality": "Village East",
      "addressRegion": "Middlesbrough",
      "postalCode": "TS4 3AE",
      "addressCountry": "GB"
    }
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@type": "OrderItem",
      "position": 0,
      "acceptedOffer": "https://example.com/api/open-active/offers/1643725800-9c44e3f8-029c-4b47-845c-52198c29ff53",
      "orderedItem": "https://example.com/api/open-active/4813dbc0-41df-42c3-aa85-cff6f89531dd/slots/f5b4d223-1c77-5e68-acbf-8624239fef13",
    },{
      "@type": "OrderItem",
      "position": 1,
      "quantity": 2,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 0
      }
    },{
      "@type": "OrderItem",
      "position": 2,
      "quantity": 3,
      "acceptedOffer": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
      "orderedItem": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 0
      }
    }
  ]
}

C1 Response

HTTP/1.1 200 OK
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@id": "https://example.com/api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8",
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    "@type": "Organization",
    ...
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "acceptedOffer": "https://example.com/api/open-active/offers/1643725800-9c44e3f8-029c-4b47-845c-52198c29ff53",
      "orderedItem": "https://example.com/api/open-active/4813dbc0-41df-42c3-aa85-cff6f89531dd/slots/f5b4d223-1c77-5e68-acbf-8624239fef13",
    },{
      "@id": "https://example.com/api/open-active/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "quantity": 2,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem"
      }
    },{
      "@id": "https://example.com/api/open-active/order-items/5ec216fd-1680-48f5-ba86-33d80b2c60f4",
      "@type": "OrderItem",
      "acceptedOffer": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
      "orderedItem": {
        "@type": "AddOn",
        "@id": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
      },
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem"
      }
    }
  ]
}

Note how the position property is dropped from the response. In future requests to C2 the broker MUST supply both position and @id in the beta:parentOrderItem so that the booking system can easily make modifications if required.

C2 Request

PUT /api/order-quote-templates/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/1.1
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    "@type": "Organization",
    "name": "MyFitnessApp",
    "url": "https://myfitnessapp.example.com",
    "description": "A fitness app for all the community",
    "logo": {
      "@type": "ImageObject",
      "url": "http://data.myfitnessapp.org.uk/images/logo.png"
    },
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "Alan Peacock Way",
      "addressLocality": "Village East",
      "addressRegion": "Middlesbrough",
      "postalCode": "TS4 3AE",
      "addressCountry": "GB"
    }
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "position": 0,
      "acceptedOffer": "https://example.com/api/open-active/offers/1643725800-9c44e3f8-029c-4b47-845c-52198c29ff53",
      "orderedItem": "https://example.com/api/open-active/4813dbc0-41df-42c3-aa85-cff6f89531dd/slots/f5b4d223-1c77-5e68-acbf-8624239fef13",
    },{
      "@id": "https://example.com/api/open-active/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "position": 1,
      "quantity": 2,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem",
        "position": 0
      }
    },{
      "@type": "OrderItem",
      "position": 2,
      "quantity": 3,
      "acceptedOffer": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
      "orderedItem": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem",
        "position": 0
      }
    }
  ]
}

In this request we have transparently removed the original third order item and replaced it with an identical replacement. This is to show how the booking system must take the given OrderItems and remove existing ones from its original booking model where they are replaced. This is indicated by any order item with a position property but not an @id property.

Requests at P and B will be identical when referring to AddOns.

Appendices:

R.Appendix A

Cancelling an AddOn. As described in Open Booking API 8.2, simply removing the AddOn from the next request will remove it from the Order:

PUT /api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/1.1
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    "@type": "Organization",
    "name": "MyFitnessApp",
    "url": "https://myfitnessapp.example.com",
    "description": "A fitness app for all the community",
    "logo": {
      "@type": "ImageObject",
      "url": "http://data.myfitnessapp.org.uk/images/logo.png"
    },
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "Alan Peacock Way",
      "addressLocality": "Village East",
      "addressRegion": "Middlesbrough",
      "postalCode": "TS4 3AE",
      "addressCountry": "GB"
    }
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "position": 0,
      "acceptedOffer": "https://example.com/api/open-active/offers/1643725800-9c44e3f8-029c-4b47-845c-52198c29ff53",
      "orderedItem": "https://example.com/api/open-active/4813dbc0-41df-42c3-aa85-cff6f89531dd/slots/f5b4d223-1c77-5e68-acbf-8624239fef13",
    },{
      "@id": "https://example.com/api/open-active/order-items/5ec216fd-1680-48f5-ba86-33d80b2c60f4",
      "@type": "OrderItem",
      "position": 1,
      "quantity": 3,
      "acceptedOffer": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
      "orderedItem": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem",
        "position": 0
      }
    }
  ]
}

To cancel an AddOn after B, simply use the same request as defined in the booking spec

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/1.1
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    ...
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/open-active/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    }
  ]
}

However, if you try to cancel an OrderItem which has children, Brokers MUST cancel all Order Items:

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/1.1
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    ...
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    },{
      "@id": "https://example.com/api/open-active/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    },{
      "@id": "https://example.com/api/open-active/order-items/5ec216fd-1680-48f5-ba86-33d80b2c60f4",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    }
  ]
}

As opposed to:

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/1.1
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    ...
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    }
  ]
}

Which the Booking System MUST return this error:

HTTP/1.1 400 Bad Request
Date: Mon, 8 Oct 2018 20:52:36 GMT
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrphansNotPermittedError",
  "description": "Cannot orphan AddOns without also removing those AddOns"
}

R.Appendix B

AddOns MAY be modified before B using replacement as defined above. However after B, AddOns MUST NOT increase in quantity for that Order. If more AddOns are required, the Broker will need to create a new Order. AddOns however can reduce in quantity, as this can fit nicely with the cancellation flow:

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/1.1
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    ...
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/open-active/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "quantity": 1,
      "orderItemStatus": "https://openactive.io/AddOnQuantityReduction",
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/open-active/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem"
      }
    }
  ]
}
nickevansuk commented 2 years ago

If helpful, some of my notes from the W3C call 27/04/2022 (there were other actions recorded too not captured here):

It might be useful to also - in future - consider deposits for rackets etc (e.g. via partial refunds)

It would also be helpful to have a data dump from Playfinder for the shape of Add-ons

nathansalter commented 1 year ago

Finally gone through this again, and revised with the comments from the discussions made. We're going to be continuing on with implementing this, so I'll update here with any issues we face during execution.

Add Ons

AddOns in Availability feeds

Facility Use Feeds

{
  "next": "https://example.com/api/openactive/individual-facility-uses?afterTimestamp=1643121185935915&afterId=2d21b9fb-ebb1-49b1-a07c-fdb81dba5c23",
  "items": [
    {
      "state": "updated",
      "kind": "IndividualFacilityUse",
      "id": "d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
      "modified": 1592994256385869,
      "data": {
        "@id": "https://example.com/api/openactive/fe30c0e1-b4f9-4226-9fd8-3ddb230edd9f/individual-facility-uses/d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
        "@type": "IndividualFacilityUse",
        "@context": [
          "https://openactive.io/",
          "https://openactive.io/ns-beta"
        ],
        "identifier": "d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
        "name": "Football 11-a-side",
        "url": "https://example.com/api/openactive/fe30c0e1-b4f9-4226-9fd8-3ddb230edd9f/individual-facility-uses/d7fb4341-756a-41a8-ad17-4b8f6b574bb6",
        "activity": [
          {
            "@type": "Concept",
            "@id": "https://openactive.io/activity-list#117e7f70-6c42-4b1f-a3bb-620b63ea263l",
            "inScheme": "https://openactive.io/activity-list",
            "prefLabel": "11-a-side"
          }
        ],
        "beta:addOn": [{
          "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
          "@type": "AddOn",
          "name": "Adult Racket Hire",
          "priceSpecification": {
            "eligibleQuantity": {
              "@type": "QuantitiveValue",
              "minValue": 0,
              "maxValue": 4
            }
          },
          "offers": [
            {
              "@type": "Offer",
              "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
              "priceCurrency": "GBP",
              "price": 1.5,
              "availableChannel": "https://openactive.io/OpenBookingPrepayment"
            }
          ]
        }, {
          "@id": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
          "@type": "AddOn",
          "name": "Junior Racket Hire",
          "priceSpecification": {
            "eligibleQuantity": {
              "@type": "QuantitiveValue",
              "minValue": 0,
              "maxValue": 4
            }
          },
          "offers": [
            {
              "@type": "Offer",
              "@id": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
              "priceCurrency": "GBP",
              "price": 0,
              "availableChannel": "https://openactive.io/OpenBookingPrepayment"
            }
          ]
        }],
        "hoursAvailable": [
          ...
        ],
        "location": {
          ...
        },
        "provider": {
          "@type": "Organization",
          "@id": "https://example.com/api/venues/fe30c0e1-b4f9-4226-9fd8-3ddb230edd9f",
          "name": "Sundridge Park Lawn Tennis & Squash Rackets Club"
        }
      }
    }
  ],
  "license": "https://creativecommons.org/licenses/by/4.0/"
}

Objects in the beta:addOn property indicate that the facility can provide AddOns with the provided offers.

You MAY also specify advanceBooking as https://openactive.io/Unavailable to signify that the AddOn is available but not bookable e.g. if the facility provides coin-operated lockers, but you can't pay for them in advance.

Brokers SHOULD offer these at checkout for the customer to optionally select. The minimum eligibleQuantity MAY be above zero to indicate that this is a required AddOn. It is RECOMMENDED that when this is the case, brokers include this extra cost (if applicable) into the cost shown to the customer before slot selection takes place. The Broker MUST adhere to the minimum/maximum quantity values shown here when submitting Order requests at C1, C2, B etc.

Session Series Feeds

{
  "next": "https://example.com/api/openactive/schedules?afterTimestamp=1643121185935915&afterId=2d21b9fb-ebb1-49b1-a07c-fdb81dba5c23",
  "items": [
    {
      "state": "updated",
      "kind": "SessionSeries",
      "id": "756f1b7a-df67-4d4d-8d81-fcc2b169f212",
      "modified": 1676375547094113,
      "data": {
        "@type": "SessionSeries",
        "@context": [
          "https://openactive.io/",
          "https://openactive.io/ns-beta"
        ],
        "@id": "https://example.com/api/openactive/42b3928b-4613-4e9d-a086-54d8c76cf8ae/session-series/756f1b7a-df67-4d4d-8d81-fcc2b169f212",
        "identifier": "756f1b7a-df67-4d4d-8d81-fcc2b169f212",
        "name": "Organiser Email check",
        "description": "Testing",
        "url": "https://example.com",
        "offers": [
          {
            "@type": "Offer",
            "@id": "https://example.com/api/openactive/offers/756f1b7a-df67-4d4d-8d81-fcc2b169f212-0",
            "identifier": "756f1b7a-df67-4d4d-8d81-fcc2b169f212-0",
            "name": "Organiser Email check (0)",
            "url": "https://example.com/api/openactive/order-quote-templates/56bc1392-ea1a-4df1-b2dc-ffdf18c9e40b",
            "priceCurrency": "GBP",
            "price": 5
          }
        ],
        "location": {
          "@type": "Place",
          "@id": "https://example.com/api/openactive/place/42b3928b-4613-4e9d-a086-54d8c76cf8ae",
          "name": "AJ Sports Complex",
          "address": {
            "@type": "PostalAddress",
            "addressCountry": "GB",
            "addressRegion": "London",
            "addressLocality": "Greater London",
            "postalCode": "E1W 3DP",
            "streetAddress": "445 Cable Street"
          },
          "geo": {
            "@type": "GeoCoordinates",
            "latitude": 51.5114909,
            "longitude": -0.0477878
          }
        },
        "organizer": {
          "@type": "Organization",
          "@id": "https://example.com/api/venues/42b3928b-4613-4e9d-a086-54d8c76cf8ae",
          "name": "PF",
          "email": "email.check@example.com",
          "isOpenBookingAllowed": true,
          "taxMode": "https://openactive.io/TaxGross"
        },
        "eventSchedule": [
          {
            "@type": "PartialSchedule",
            "byDay": [
              "https://schema.org/Tuesday"
            ],
            "repeatCount": 5,
            "repeatFrequency": "P7D",
            "scheduleTimezone": "Europe/London",
            "startDate": "2023-02-20"
          }
        ],
        "activity": [
          {
            "@type": "Concept",
            "@id": "https://openactive.io/activity-list#c4661096-04c3-41de-b8d2-00788dd53023",
            "inScheme": "https://openactive.io/activity-list",
            "prefLabel": "Billiards"
          }
        ],
        "ageRange": {
          "@type": "QuantitativeValue",
          "maxValue": 90,
          "minValue": 18
        },
        "category": [
          "drop-in"
        ],
        "genderRestriction": "https://openactive.io/MaleOnly",
        "level": [
          "intermediate"
        ],
        "beta:offerValidityPeriod": "P1D",
        "beta:addOn": [{
          "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
          "@type": "AddOn",
          "name": "Adult Racket Hire",
          "priceSpecification": {
            "eligibleQuantity": {
              "@type": "QuantitiveValue",
              "minValue": 0,
              "maxValue": 4
            }
          },
          "offers": [
            {
              "@type": "Offer",
              "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
              "priceCurrency": "GBP",
              "price": 1.5
            }
          ]
        }]
      }
    }
  ],
  "license": "https://creativecommons.org/licenses/by/4.0/"
}

AddOns in Booking Flow

And Stage 2 adding this into the booking APIs:

image

In the image above, it indicates how the structure of the order items is assumed to be. Brokers MUST only use a single level of nesting.

The standard booking api can be updated to the following:

C1 Request

PUT /api/order-quote-templates/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    "@type": "Organization",
    "name": "MyFitnessApp",
    "url": "https://myfitnessapp.example.com",
    "description": "A fitness app for all the community",
    "logo": {
      "@type": "ImageObject",
      "url": "http://data.myfitnessapp.org.uk/images/logo.png"
    },
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "Alan Peacock Way",
      "addressLocality": "Village East",
      "addressRegion": "Middlesbrough",
      "postalCode": "TS4 3AE",
      "addressCountry": "GB"
    }
  },
  "seller": "https://example.com/api/organisations/123",
  "orderedItem": [
    {
      "@type": "OrderItem",
      "position": 0,
      "acceptedOffer": "https://example.com/events/452#/offers/878",
      "orderedItem": "https://example.com/events/452/subEvents/132"
    },
    {
      "@type": "OrderItem",
      "position": 1,
      "quantity": 2,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 0
      }
    },
    {
      "@type": "OrderItem",
      "position": 2,
      "quantity": 3,
      "acceptedOffer": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
      "orderedItem": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 0
      }
    }
  ]
}

C1 Response

HTTP/2 200 OK
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "@id": "https://example.com/api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8",
  "orderRequiresApproval": false,
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    ...
  },
  "seller": {
    ..
  },
  "bookingService": {
    "@type": "BookingService",
    "name": "Playwaze",
    "url": "http://www.playwaze.com",
    "termsOfService": [
      {
        "@type": "Terms",
        "name": "Terms of Service",
        "url": "https://brokerexample.com/terms.html",
        "requiresExplicitConsent": false
      }
    ]
  },
  "lease": {
    "@type": "Lease",
    "leaseExpires": "2018-10-01T11:00:00Z"
  },
  "orderedItem": [
    {
      "@type": "OrderItem",
      "position": 0,
      "unitTaxSpecification": [
        {
          "@type": "TaxChargeSpecification",
          "name": "VAT at 20%",
          "price": 1,
          "priceCurrency": "GBP",
          "rate": 0.2
        }
      ],
      "acceptedOffer": {
        "@type": "Offer",
        "@id": "https://example.com/events/452#/offers/878",
        "description": "Winger space for Speedball.",
        "name": "Speedball winger position",
        "price": 10,
        "priceCurrency": "GBP",
        "validFromBeforeStartDate": "P6D",
        "allowCustomerCancellationFullRefund": true,
        "latestCancellationBeforeStartDate": "P1D"
      },
      "orderedItem": {
        "@type": "ScheduledSession",
        "@id": "https://example.com/events/452/subEvents/132",
        "identifier": 123,
        "eventStatus": "https://schema.org/EventScheduled",
        "maximumAttendeeCapacity": 30,
        "remainingAttendeeCapacity": 20,
        ...
      }
    },
    {
      "@type": "OrderItem",
      "position": 1,
      "quantity": 2,
      "acceptedOffer": {
        "@type": "Offer",
        "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
        "priceCurrency": "GBP",
        "price": 1.5
      },
      "orderedItem": {
        "@id": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
        "@type": "AddOn",
        "name": "Adult Racket Hire"
      },
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 0
      }
    },
    {
      "@type": "OrderItem",
      "position": 2,
      "quantity": 3,
      "acceptedOffer": {
        "@type": "Offer",
        "@id": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
        "priceCurrency": "GBP",
        "price": 1.5
      },
      "orderedItem": {
        "@id": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
        "@type": "AddOn",
        "name": "Junior Racket Hire"
      },
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 0
      }
    }
  ],
  "totalPaymentDue": {
    "@type": "PriceSpecification",
    "price": 5,
    "priceCurrency": "GBP"
  },
  "totalPaymentTax": [
    {
      "@type": "TaxChargeSpecification",
      "name": "VAT at 20%",
      "price": 1,
      "priceCurrency": "GBP",
      "rate": 0.2
    }
  ]
}

C2 Request

PUT /api/order-quote-templates/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    "@type": "Organization",
    "name": "MyFitnessApp",
    "url": "https://myfitnessapp.example.com",
    "description": "A fitness app for all the community",
    "logo": {
      "@type": "ImageObject",
      "url": "http://data.myfitnessapp.org.uk/images/logo.png"
    },
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "Alan Peacock Way",
      "addressLocality": "Village East",
      "addressRegion": "Middlesbrough",
      "postalCode": "TS4 3AE",
      "addressCountry": "GB"
    }
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "position": 0,
      "acceptedOffer": "https://example.com/api/openactive/offers/1643725800-9c44e3f8-029c-4b47-845c-52198c29ff53",
      "orderedItem": "https://example.com/api/openactive/4813dbc0-41df-42c3-aa85-cff6f89531dd/slots/f5b4d223-1c77-5e68-acbf-8624239fef13",
    },{
      "@id": "https://example.com/api/openactive/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "position": 1,
      "quantity": 2,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem",
        "position": 0
      }
    },{
      "@type": "OrderItem",
      "position": 2,
      "quantity": 3,
      "acceptedOffer": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
      "orderedItem": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem",
        "position": 0
      }
    }
  ]
}

In this request we have transparently removed the original third order item and replaced it with an identical replacement. This is to show how the booking system must take the given OrderItems and remove existing ones from its original booking model where they are replaced. This is indicated by any order item with a position property but not an @id property.

Requests at P and B will be identical when referring to AddOns.

Appendices:

Appendix A

Cancelling an AddOn. As described in Open Booking API 8.2, simply removing the AddOn from the next request will remove it from the Order:

PUT /api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    "@type": "Organization",
    "name": "MyFitnessApp",
    "url": "https://myfitnessapp.example.com",
    "description": "A fitness app for all the community",
    "logo": {
      "@type": "ImageObject",
      "url": "http://data.myfitnessapp.org.uk/images/logo.png"
    },
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "Alan Peacock Way",
      "addressLocality": "Village East",
      "addressRegion": "Middlesbrough",
      "postalCode": "TS4 3AE",
      "addressCountry": "GB"
    }
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "position": 0,
      "acceptedOffer": "https://example.com/api/openactive/offers/1643725800-9c44e3f8-029c-4b47-845c-52198c29ff53",
      "orderedItem": "https://example.com/api/openactive/4813dbc0-41df-42c3-aa85-cff6f89531dd/slots/f5b4d223-1c77-5e68-acbf-8624239fef13",
    },{
      "@id": "https://example.com/api/openactive/order-items/5ec216fd-1680-48f5-ba86-33d80b2c60f4",
      "@type": "OrderItem",
      "position": 1,
      "quantity": 3,
      "acceptedOffer": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944#offer",
      "orderedItem": "https://example.com/openactive/add-ons/b9372482-8d6e-4d89-9160-58fa52d86944",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem",
        "position": 0
      }
    }
  ]
}

To cancel an AddOn after B, simply use the same request as defined in the booking spec

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "Order",
  "orderedItem": [
    {
      "@id": "https://example.com/api/openactive/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    }
  ]
}

However, if you try to cancel an OrderItem which has children, Brokers MUST cancel all Order Items:

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "Order",
  "orderedItem": [
    {
      "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    },{
      "@id": "https://example.com/api/openactive/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    },{
      "@id": "https://example.com/api/openactive/order-items/5ec216fd-1680-48f5-ba86-33d80b2c60f4",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    }
  ]
}

As opposed to:

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "Order",
  "orderedItem": [
    {
      "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
      "@type": "OrderItem",
      "orderItemStatus": "https://openactive.io/CustomerCancelled"
    }
  ]
}

To which the Booking System MUST return this error:

HTTP/2 400 Bad Request
Date: Mon, 8 Oct 2018 20:52:36 GMT
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrphansNotPermittedError",
  "description": "Cannot orphan OrderItems without also removing those OrderItems"
}

Order Items cannot be created without a parent:

PUT /api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  ...
  "orderedItem": [
    {
      "@type": "OrderItem",
      "position": 1,
      "quantity": 1,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 10000
      }
    }
  ]
}

To which the Booking System MUST return this error:

HTTP/2 400 Bad Request
Date: Mon, 8 Oct 2018 20:52:36 GMT
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrphansNotPermittedError",
  "description": "Cannot create OrderItems with a parent that does not exist"
}

Appendix B

AddOns MAY be modified before B using replacement as defined above. However after B, AddOns MUST NOT increase in quantity for that Order. If more AddOns are required, the Broker will need to create a new Order. AddOns however can reduce in quantity, as this can fit nicely with the cancellation flow:

PATCH /api/orders/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "brokerRole": "https://openactive.io/AgentBroker",
  "broker": {
    ...
  },
  "seller": {
    "@type": "Organization",
    "@id": "https://example.com/api/organisations/123"
  },
  "orderedItem": [
    {
      "@id": "https://example.com/api/openactive/order-items/08d7e6a6-757f-4dd5-af0e-d1692bbd21c7",
      "@type": "OrderItem",
      "quantity": 1,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@id": "https://example.com/api/openactive/order-items/c67de59f-3f32-4751-ac4d-59431a13be0d",
        "@type": "OrderItem"
      }
    }
  ]
}

Appendix C

Order Items cannot be forced to create a recursive tree:

PUT /api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  ...
  "orderedItem": [
    {
      "@type": "OrderItem",
      "position": 1,
      "quantity": 1,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 1
      }
    }
  ]
}

To which the Booking System MUST return this error:

HTTP/2 400 Bad Request
Date: Mon, 8 Oct 2018 20:52:36 GMT
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "RecursiveOrderItemError",
  "description": "Cannot create an OrderItem which references itself"
}

Order Items cannot be created at multiple levels of inheritance. This is partly for simplicity, especially with the above error if A is a child of B which is in turn a Child of A, the logic to detect this becomes needlessly complex for a use-case which we have been unable to come up with.

PUT /api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8 HTTP/2
Host: example.com
Date: Mon, 8 Oct 2018 20:52:35 GMT
Accept: application/vnd.openactive.booking+json; version=1
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  ...
  "orderedItem": [
    {
      "@type": "OrderItem",
      "position": 0,
      "quantity": 1,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
    },
    {
      "@type": "OrderItem",
      "position": 1,
      "quantity": 1,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 0
      }
    },
    {
      "@type": "OrderItem",
      "position": 2,
      "quantity": 1,
      "acceptedOffer": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603#offer",
      "orderedItem": "https://example.com/openactive/add-ons/de505ce7-351f-4f8f-90c4-29887947a603",
      "beta:parentOrderItem": {
        "@type": "OrderItem",
        "position": 1
      }
    }
  ]
}

To which the Booking System MUST return this error:

HTTP/2 400 Bad Request
Date: Mon, 8 Oct 2018 20:52:36 GMT
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "UnexpectedOrderItemGrandparentError",
  "description": "Cannot create an OrderItem which references a child of another OrderItem"
}

Appendix D

If the AddOn has a stock level which can run out, when running C1 or C2 then the response MUST respond with an OpportunityHasInsufficientCapacityError.

HTTP/2 409 Conflict
Date: Mon, 8 Oct 2018 20:52:36 GMT
Content-Type: application/vnd.openactive.booking+json; version=1

{
  "@context": "https://openactive.io/",
  "@type": "OrderQuote",
  "@id": "https://example.com/api/order-quotes/e11429ea-467f-4270-ab62-e47368996fe8",
  ...
  "orderedItem": [
    {
      ..
      "error": [
        {
          "@type": "OpportunityHasInsufficientCapacityError",
          "name": "There is an insufficient quantity of this item available to book",
          "statusCode": 409
        }
      ]
    }
  ],
}