-
Notifications
You must be signed in to change notification settings - Fork 3
Description
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:
OrderQuoteis defined as a quote within the source system, and is not storedOrderQuotehas 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:
OrderQuotemust 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
OrderQuoteby 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/OrderItemCombinationIncompatiblehttps://openactive.io/OrderFulfillablehttps://openactive.io/OrderIncorrectPricehttps://openactive.io/OrderTooManyItems
"orderItemFulfillability": "https://openactive.io/OrderItemFulfillable" has the following options:
https://openactive.io/OrderItemFulfillablehttps://openactive.io/OrderItemCombinationIncompatiblehttps://openactive.io/OrderItemUnavailablehttps://openactive.io/OrderItemIncorrectPricehttps://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
itemPriceon theOrderItems passed into the request, and by providing anitemPricein 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?