Skip to content

OrderQuote #87

@nickevansuk

Description

@nickevansuk

Proposer

ODI

Use Case

To allow for availability / fulfillability of an order to be confirmed, as per the flow suggested in #66, it must be possible to submit an order and have a totalPaymentDue returned along with the confirmation of OrderItem availability.

Why is this needed?

Currently availability is based on an individual session level only, and does not allow the confirmation of totalPaymentDue to factor in any dynamic pricing changes - for example in the case that a 2-for-1 discount is applied.

Proposal

Modelling options: either a new type OrderQuote, or a new orderStatus of "https://openactive.io/OrderQuote" on Order

The OrderQuote returns not only the correct totalPaymentDue, but also confirms that a specific group of OrderItems can be purchased together in the quantities desired. For example, if an OrderQuote was requested with OrderItems for both a badminton court and a table tennis table that used the same physical space, the OrderQuote should return an error expressing the constraint that they cannot both be included in the Order.

Move the current section of 7.1.3 Checking availability and offers for an event or facility use slot into the future Opportunity API, and replace it with an explanation of OrderQuote (issue filed separately for this here: #91).

This also provides a clean separation between the Open Booking API, the Opportunity API, and the open feeds.

OrderQuote Example

A new endpoint /quotes that accepts a POST of type OrderQuote, and returns an OrderQuote with the relevant information.

OrderQuote subclasses Order, with the following differences:

  • OrderQuote is defined as a quote within the source system, and is not stored
  • OrderQuote has different fields required

Note that orderedItem.orderedItem within OrderQuote is expected to be expanded to the full Event (although this should be done for all Order responses: #84).

Note that both types of terms and conditions are also included as per #60.

Open feed

The potentialActions require updating to use schema.org's QuoteAction and BuyAction.

Note regarding optional support:

  • OrderQuote must always be implemented by the Booking System, however the Broker is not required to call it in all cases - for example, in the case of free bookings where the user has already registered details, or where the Broker has been informed of the totalPaymentDue via other means such as using the Opportunity API or other non-OpenActive APIs.
  • However the use of OrderQuote by the Broker is always highly RECOMMENDED to ensure that the correct pricing is being used - including tax and other considerations - in order to avoid failed bookings.
  • Although the Broker must pass a unique and consistent UUID in the OrderQuote and subsiquent Order, the Booking System can choose whether or not to implement leasing based on this UUID.

Discovering the quote endpoint

The Event will contain:

  "potentialAction": [
    {
      "type": "QuoteAction",
      "name": "Quote",
      "target": {
        "type": "EntryPoint",
        "urlTemplate": "https://example.com/quotes",
        "encodingType": "application/vnd.openactive.v1.0+json",
        "httpMethod": "POST"
      }
    },
    {
      "type": "BuyAction",
      "name": "Book",
      "target": {
        "type": "EntryPoint",
        "urlTemplate": "https://example.com/orders",
        "encodingType": "application/vnd.openactive.v1.0+json",
        "httpMethod": "POST"
      }
    }
  ]

Request

POST /quotes HTTP/1.1

{
  "@context": "https://openactive.io/",
  "type": "OrderQuote",
  "orderNumber": "urn:uuid:123e4567-e89b-12d3-a456-426655440000",
  "orderedItem": [
    {
      "type": "OrderItem",
      "orderQuantity": 1,
      "acceptedOffer": "https://example.com/offers/1234",
      "orderedItem": "https://example.com/events/452"
    }
  ]
}

Response

HTTP/1.1 200 OK
Date: Mon, 8 Oct 2018 20:50:36 GMT
Content-Type: application/vnd.openactive.v1.0+json
Content-Length: ...
{
  "@context": "https://openactive.io/",
  "type": "OrderQuote",
  "orderNumber": "urn:uuid:123e4567-e89b-12d3-a456-426655440000",
  "orderLeaseExpiry": "2018-11-04T15:31:21Z",
  "orderedItem": [
    {
      "type": "OrderItem",
      "orderQuantity": 1,
      "acceptedOffer": "https://example.com/events/452#/offers/878",
      "orderedItem": {
        "type": "ScheduledSession",
        "identifier": 123,
        "id": "https://example.com/events/452/subEvents/132",
        "eventStatus": "https://schema.org/EventScheduled",
        "maximumAttendeeCapacity": 30,
        "remainingAttendeeCapacity": 20,
        "startDate": "2018-10-30T11:00:00Z",
        "endDate": "2018-10-30T12:00:00Z",
        "duration": "PT1H",
        "superEvent": {
          "type": "SessionSeries",
          "id": "https://example.com/events/452",
          "name": "Speedball",
          "offers": [
            {
              "type": "Offer",
              "id": "https://example.com/events/452#/offers/878",
              "validThrough": "2018-10-29T11:00:00Z",
              "validFrom": "2018-10-01T11:00:00Z",
              "description": "Winger space for Speedball.",
              "name": "Speedball winger position",
              "price": 10.00,
              "priceCurrency": "GBP",
              "isCancellable": true,
              "cancellationValidUntil": "2018-10-28T11:00:00Z"
            }
          ],
          "duration": "PT1H",
          "organizer": {
            "type": "Organization",
            "name": "Central Speedball Association",
            "url": "http://www.speedball-world.com",
            "termsOfService": { "type": "TermsOfService", "url": "url"}
          },
          "location": {},
        }
      }
    }
  ],
  "orderStatus": "https://openactive.io/OrderQuote",
  "partOfInvoice": {
    "type": "Invoice",
    "paymentStatus": "https://openactive.io/PaymentQuote",
    "totalPaymentDue": {
      "type": "MonetaryAmount",
      "value": 10.00,
      "currency": "GBP"
    }
  },
  "bookingService": {
    "type": "BookingService",
    "name": "Playwaze",
    "url": "http://www.playwaze.com",
    "termsOfService": { "type": "TermsOfService", "url": "url" }
  },
  "potentialAction": [
    {
      "type": "BuyAction",
      "name": "Book",
      "target": {
        "type": "EntryPoint",
        "urlTemplate": "https://example.com/orders",
        "encodingType": "application/vnd.openactive.v1.0+json",
        "httpMethod": "POST"
      }
    }
  ]
}

Error states

Taking inspiration from Google Reserve, in the case an order can't be fulfilled:

"orderFulfillability": "https://openactive.io/OrderFulfillable"

  • https://openactive.io/OrderItemCombinationIncompatible
  • https://openactive.io/OrderFulfillable
  • https://openactive.io/OrderIncorrectPrice
  • https://openactive.io/OrderTooManyItems

"orderItemFulfillability": "https://openactive.io/OrderItemFulfillable" has the following options:

  • https://openactive.io/OrderItemFulfillable
  • https://openactive.io/OrderItemCombinationIncompatible
  • https://openactive.io/OrderItemUnavailable
  • https://openactive.io/OrderItemIncorrectPrice
  • https://openactive.io/OrderItemExcessiveQuantity

The strings "orderFulfillabilityReason" and "orderItemFulfillabilityReason" are also available to provide relevant error messages to the end user.

Implementation notes

  • UUIDs should never be relied on in a way that could create a security threat. Hence any implementation must store leases against UUIDs within bounds of a particular API user. UUID forgery MUST NOT give API users access to leases from other API users.
  • Broker implementations MUST generate UUID on the server side on behalf of their users and not rely on client-side libraries to do so, as the random number generators of mobile devices vary considerably and should not be relied upon.

Open Questions

  • Unlike Google Reserve, this specification does not require an exact match to the price presented to the consumer, as long as it is cheaper than the amount presented. It would be easy to add such functionality be requiring an itemPrice on the OrderItems passed into the request, and by providing an itemPrice in the response. Is it a legal requirement for the user to pay exactly what they expected, or is paying less for the same items allowable?
    • Note that Legend have previously requested adding an item price for this purpose.
  • For simple implementations, multiple items and/or quantity > 1 may not be supported. Should we include a flag somewhere to prevent a broker attempting to create an invalid Order? Or is the fulfillability error returned for more than one order sufficient? Could use Order.eligibleQuantity?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions