Open markthomasUprise opened 6 years ago
Really great idea! Potentially we could use membership type IDs as references here, so something like the below? This way a membership such as https://api.example.com/memberships/1
could have an offer, which could be purchased, providing an easy route to "upsell" memberships to users looking at Pay as You Go.
So within GET /sessions
:
"offers": [
{
"name": "Non-member price",
"price": 4.5,
"priceCurrency": "GBP"
},
{
"name": "Member price",
"price": 4.5,
"priceCurrency": "GBP",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembership": [
"https://api.example.com/memberships/1",
"https://api.example.com/memberships/2"
],
},
{
"name": "Member price",
"price": 3.5,
"priceCurrency": "GBP",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembership": [
"https://api.example.com/memberships/3",
"https://api.example.com/memberships/4"
],
}
],
And as a response to GET /memberships/1
(taking inspiration from this proposal):
{
"type": "Membership",
"id": "https://api.example.com/memberships/1",
"name": "Off-Peak Fitness",
"description": "Unlimited access to the gym, fitness classes, swimming pool and select facilities at off-peak times",
"offers": [
{
"price": 45,
"priceCurrency": "GBP",
"paymentFrequency": "http://openactive.io/ns#Monthly",
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/4"
}
]
}
(N.B. we probably need a whole new proposal for memberships, which includes potentialActions to allow them to be bookable natively. We also need to think about how they can be exposed as open data so they can be referenced from the main session feed.)
One route might be to embed memberships in the feed itself as part of the event:
"offers": [
{
"name": "Non-member price",
"price": 4.5,
"priceCurrency": "GBP"
},
{
"name": "Member price",
"price": 4.5,
"priceCurrency": "GBP",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembership": [
"https://api.example.com/memberships/1",
"https://api.example.com/memberships/2"
],
},
{
"name": "Member price",
"price": 3.5,
"priceCurrency": "GBP",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembership": [
"https://api.example.com/memberships/3",
"https://api.example.com/memberships/4"
],
}
],
"memberships": [
{
"type": "Membership",
"id": "https://api.example.com/memberships/1",
"name": "Off-Peak Fitness",
"description": "Unlimited access to the gym, fitness classes, swimming pool and select facilities at off-peak times",
"offers": [
{
"price": 45,
"priceCurrency": "GBP",
"paymentFrequency": "http://openactive.io/ns#Monthly",
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/4"
}
]
}
]
Spec will also need to cover those venues who do not wish to make their member rates public.
Some venues will not be happy revealing grandfathered, exclusive, etc rates. In those cases, I think we can just leave "price" off the entry, as such:
"offers": [
{
"name": "Non-member price",
"price": 4.5,
"priceCurrency": "GBP"
},
{
"name": "Member price",
"price": 4.5,
"priceCurrency": "GBP",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembership": [
"https://api.example.com/memberships/1",
"https://api.example.com/memberships/2"
],
},
{
"name": "Private Member price",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembership": [
"https://api.example.com/memberships/3",
"https://api.example.com/memberships/4"
],
}
],
"memberships": [
{
"type": "Membership",
"id": "https://api.example.com/memberships/1",
"name": "Off-Peak Fitness",
"description": "Unlimited access to the gym, fitness classes, swimming pool and select facilities at off-peak times",
"offers": [
{
"price": 45,
"priceCurrency": "GBP",
"paymentFrequency": "http://openactive.io/ns#Monthly",
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/4"
}
]
}
]
As a general point of process, I think there's actually two aspects to this proposal:
how do we describe member pricing and membership options within Offers. This is an extension/refinement to the modelling opportunity data specification.
how do we support bookings by members / existing account holders within the booking specification? This would include ensuring that clients consistently apply rules to recommend prices and potentially checking membership/account holder status of a user.
I want to flag that the second aspect is currently out of scope for v1.0 of the booking API. This is based on the requirements and use cases we have agreed with the community.
Suggest we continue to discuss the changes to the data model here, but noting that the initial changes may be to make this information available as open data before its supported in booking.
Noting here some overlap with https://github.com/openactive/open-booking-api/issues/60, where memberships
might be better sat inside organizer
Also noting that schema.org's ProgramMembership
could be used or subclassed for this purpose.
Also note an exact mapping between the memberships
and eligibleMembership
approach above and Google Reserve's PaymentOption
:
Google Reserve Merchant
~= OpenActive Event.organizer
+ OpenActive Event.location
Google Reserve Service
~= OpenActive Event
Google Reserve Merchant.PaymentOption
~= OpenActive Event.organizer.membership
(this proposal)
Google Reserve Service.payment_option_id
~= OpenActive Event.offers.eligibleMembership
(this proposal)
Further notes on this considering properties that should be included in a membership; we should consider including:
availabilityStarts
"The end of the availability of the product or service included in the offer." and availabilityEnds
validFrom
"The date when the item becomes valid." and validThrough
eligibleDuration
"The duration for which the given offer is valid."eligibleQuantity
"The interval and unit of measurement of ordering quantities for which the offer or price specification is valid. This allows e.g. specifying that a certain freight charge is valid only for a certain quantity."availabilityStarts
and validFrom
See https://github.com/openactive/open-booking-api/issues/59
eligibleDuration
From the [GoodRelations definition of eligibleDuration
] (http://www.heppnetz.de/ontologies/goodrelations/v1.html#eligibleDuration):
The minimal and maximal duration for which the given gr:Offering or gr:License is valid. This is mostly used for offers regarding accommodation, the rental of objects, or software licenses. The duration is specified by attaching an instance of gr:QuantitativeValue. The lower and upper boundaries are specified using the properties gr:hasMinValue and gr:hasMaxValue to that instance. If they are the same, use the gr:hasValue property. The unit of measurement is specified using the property gr:hasUnitOfMeasurement with a string holding a UN/CEFACT code suitable for durations, e.g. MON (months), DAY (days), HUR (hours), or MIN (minutes).
So this can represent Google Reserve valid_duration_sec
: "Duration of the payment option validity (e.g. 30 day membership).".
Note that eligibleDuration
should be restricted to QuantitativeValue
with a value
and unitCode
, rather than allowing a range. So only represent 30 day membership
, as a 30-60 day membership
is not a usecase we're aware of and will likely cause confusion. The unitCode
should be restricted to only temporal codes (e.g. MON (months), DAY (days)). We should also consider allowing for Duration
to be used instead of QuantitativeValue as a value eligibleDuration
to be consistent with durations elsewhere.
eligibleQuantity
This can represent Google Reserve session_count
: "How many sessions this payment option can be used for. Valid only for multi-session / packs, where the value should be > 1."
Makesweat already has notion of passes (see https://github.com/openactive/activation/issues/86) but is not marking them up specifically.
Note that eligibleQuantity
should be restricted to QuantitativeValue
with a value
and unitCode
, rather than allowing a range. So only represent 10 session pass
, as a 10-20 session pass
is not a usecase we're aware of and will likely cause confusion. The unitCode
should be restricted to only the string C62
i.e. unit
(as explained here), to indicate number of sessions included in the pass.
Also perhaps Membership
should be named more generically to include "10 session pass" as well as just "monthly membership". So perhaps "AccessPass
"?
It would also be useful to investigate PaymentOptionType
, ActivationType
and UserPurchaseRestriction
from Google Reserve to see if we can add an equivalence here, while looking at broader use cases for these too.
Another question is whether eligibleMembership
should be able to be specified at Event
/FacilityUse
level as well as just Offer
level (as an event might be eligible in its entirety regardless of which offer is selected). Google Reserve allows this.
Similar requirement to offerTemplate
in https://github.com/openactive/open-booking-api/issues/60
To add further thoughts to this, the types of membership options seem to break out into two main categories:
Also it's become apparent that although some publishers want to specify membership options in detail, and provide the option to purchase these, other publishers might want to simply specify a "membership required" flag for an Event or Offer with a link to where to find out more information.
Some properties that might be interesting to think about (see UserPaymentOption of Google Reserve):
remainingUses
?)maximumUses
?)Another two type of "membership" / booking constraints to consider:
Both of the above constraints could be satisfied by an additional property on the "Offer"
.
Further considerations as we move towards a concrete proposal:
This also mirrors what is described in Google Reserve: https://github.com/openactive/open-booking-api/issues/45#issuecomment-419706798
For large leisure operators, there a large number of possible memberships available across their Places, within the same Seller. For smaller organisations, it is likely that a Seller will have only a limited number of memberships (that may still be limited to Places.
A membership could be described in some detail, with a number of pricing options, and therefore the data object could be of a reasonable size.
Embed memberships data in existing opportunity feeds
A Sellers feed could be created that includes embedded membership data relating to each Seller. This also removes the need for Seller data to be included in every opportunity.
There has been discussion during a larger implementation on the advantages of encouraging the use of a Seller feed, for the same reason that a Places feed is beneficial.
A separate memberships feed could be created, which references the Sellers
Memberships could be embedded in Place
s, and therefore included in a Place
feed.
For reference, for this option, the proposal below would be adjusted with the below in place of the MembershipProduct feed example.
Place
(which is a common use case)Place
(e.g. a membership to a martial arts club that operates independently out of a leisure centre)Option 3 appears to have the best balance of working for independent session operators, facilities-based venues, and large leisure operators.
(However note that Option 2 or 4 might be more straightforward for smaller booking systems, so perhaps should be supported alongside or in place of Option 3. A consideration relating to this more pluralistic approach is that it creates additional complexity for Brokers that are aggregating across multiple Booking Systems - as they would need to dereference a MembershipProduct
@id
across both the Seller and the Place
- though perhaps it also simplifies the Broker's task, as they can simply display the MembershipProduct
s at the level that they are included in the data - either within a Place
or a Seller).
Following on from the modelling discussion above, a membership can therefore be represented in a feed:
{
"next": "https://www.example.com/api/rpde/membership-products?afterTimestamp=1521565719&afterId=1402CBP20150217",
"items": [
{
"state": "updated",
"kind": "MembershipProduct",
"id": "1402CBP20150217",
"modified": 1521565719,
"data": {
"@context": "https://openactive.io/",
"@type": "MembershipProduct",
"@id": "https://api.example.com/membership-products/1",
"name": "Off-Peak Fitness",
"description": "Unlimited access to the gym, fitness classes, swimming pool and select facilities at off-peak times",
"hostingOrganization": "https://api.example.com/sellers/1",
"membershipProductType": "https://openactive.io/MembershipSubscription",
"offers": [
{
"@type": "Offer",
"price": 45,
"priceCurrency": "GBP",
"paymentFrequency": "http://openactive.io/ns#Monthly",
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/4"
}
]
}
},
{
"state": "updated",
"kind": "MembershipProduct",
"id": "1402CBP20150218",
"modified": 1521565719,
"data": {
"@context": "https://openactive.io/",
"@type": "MembershipProduct",
"@id": "https://api.example.com/membership-products/2",
"name": "Yoga Off-Peak 5 Session Pass",
"description": "5 yoga sessions at select facilities at off-peak times",
"hostingOrganization": "https://api.example.com/sellers/1",
"membershipProductType": "https://openactive.io/MembershipPass",
"offers": [
{
"@type": "Offer",
"@id": "https://api.example.com/membership-products/2#/offers/1",
"price": 90,
"priceCurrency": "GBP",
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/8"
}
]
}
},
{
"state": "updated",
"kind": "MembershipProduct",
"id": "1402CBP20150219",
"modified": 1521565719,
"data": {
"@context": "https://openactive.io/",
"@type": "MembershipProduct",
"@id": "https://api.example.com/membership-products/3",
"name": "Family Membership",
"description": "The family membership entitles the whole household to book a court to play tennis for up to 1 hour a day, 5 times a week, all year round for a £55 fee.",
"hostingOrganization": "https://api.example.com/sellers/1",
"membershipProductType": "https://openactive.io/MembershipPass",
"duration": "P12M",
"offers": [
{
"@type": "Offer",
"@id": "https://api.example.com/membership-products/3#/offers/1",
"price": 55,
"priceCurrency": "GBP",
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/8"
}
]
}
},
{
"state": "updated",
"kind": "MembershipProduct",
"id": "1402CBP20150220",
"modified": 1521565719,
"data": {
"@context": "https://openactive.io/",
"@type": "MembershipProduct",
"@id": "https://api.example.com/membership-products/4",
"name": "Junior Membership",
"description": "Our junior membership is available for free to individuals under 16 and will enable young people to book and play tennis for 1 hour a day 5 times a week. This includes juniors playing with an adult.",
"hostingOrganization": "https://api.example.com/sellers/1",
"membershipProductType": "https://openactive.io/MembershipPass",
"duration": "P12M",
"ageRestriction": {
"@type": "QuantitativeValue",
"maxValue": 15
},
"offers": [
{
"@type": "Offer",
"@id": "https://api.example.com/membership-products/4#/offers/1",
"price": 0,
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/8"
}
]
}
},
{
"state": "updated",
"kind": "MembershipProduct",
"id": "1402CBP20150221",
"modified": 1521565719,
"data": {
"@context": "https://openactive.io/",
"@type": "MembershipProduct",
"@id": "https://api.example.com/membership-products/5",
"name": "Female Concession Fitness",
"description": "For females over 60 years of age. Unlimited access to the gym, fitness classes, swimming pool and select facilities at off-peak times",
"genderRestriction": "https://openactive.io/FemaleOnly",
"ageRestriction": {
"@type": "QuantitativeValue",
"minValue": 60
},
"hostingOrganization": "https://api.example.com/sellers/1",
"membershipProductType": "https://openactive.io/MembershipSubscription",
"offers": [
{
"@type": "Offer",
"price": 25,
"priceCurrency": "GBP",
"paymentFrequency": "http://openactive.io/ns#Monthly",
"url": "https://bookingsystem.example.com/membership/purchase/deep/link/16"
}
]
}
},
],
"license": "https://creativecommons.org/licenses/by/4.0/"
}
The membership can also be referenced both within Places and opportunities to which it applies, such that it can be displayed when appropriate (for example as shown in the PDF flow attached here):
{
"@type": "Places",
...
"eligibleMembershipProduct": [
"https://api.example.com/membership-products/3",
"https://api.example.com/membership-products/4"
],
...
}
{
"@type": "SessionSeries",
...
"eligibleMembershipProduct": [
"https://api.example.com/membership-products/3",
"https://api.example.com/membership-products/4"
],
...
}
{
"@type": "FacilityUse",
...
"eligibleMembershipProduct": [
"https://api.example.com/membership-products/3",
"https://api.example.com/membership-products/4"
],
...
}
A broker would be expected to display the memberships only at the level that they are described in the data i.e.
MembershipProduct
s relating to the seller
can be displayedPlace
, all MembershipProduct
s relating to the Place
(via eligibleMembershipProduct
) can be displayedMembershipProduct
s relating to the opportunity (via eligibleMembershipProduct
) can be displayedA broker SHOULD NOT infer that a MembershipProduct
is appropriate for an opportunity or Place unless is explicitly linked via eligibleMembershipProduct
.
A membership with an Offer
with an @id
may be booked through the Open Booking API in the usual way using the MembershipProduct
@id
as the opportunity ID.
schema:hostingOrganization
from schema:ProgramMembership
has been usedmembershipProductType
is proposed with values of "https://openactive.io/MembershipSubscription"
and "https://openactive.io/MembershipPass"
(to handle this requirement)ageRestriction
and genderRestriction
) are proposed to be applied to MembershipProduct
for this purpose.MembershipProduct
is used to distinguish between a particular instance of a membership (an Entitlement
, as per the Customer Accounts API) and the generic offer of the membership (which is what the MembershipProduct
refers to), while avoiding the potentially confusing term "Membership". For reference, the schema.org schema:ProgramMembership
appears to describe the instance.schema:duration
is proposed for the length of time for which a pass is valid once purchased, on the basis that the "duration" of the MembershipProduct
is as inherent to the nature of the thing being described as the "duration" of e.g. a movie - it does not have a "start" and "end" date/time (schema:validFrom
and schema:validThrough
) in the same way that a Movie
does not have a defined "start" and "end" date/time (the instance of the movie playing will do, as an Entitlement
does here). Additionally, both schema:validFor
and schema:eligibleDuration
could be interpreted as the duration that the MembershipProduct
as described is itself valid, or that the instance of the MembershipProduct
is valid. (Feedback very welcome on this: an argument for schema:validFor
could easily be made on the basis that schema:validFrom
and schema:validThrough
are defined on the Entitlement
, and therefore schema:validFor
on the Entitlement
would have the same value as duration
here.)Using the patterns already included in the Customer Accounts API, the existing use of eligibleCustomerType
beta, and referencing in the above discussion, Offer
s that are restricted to specific memberships may be included with opportunities.
"offers": [
{
"name": "Non-member price",
"price": 4.5,
"priceCurrency": "GBP"
},
{
"name": "Member price",
"price": 4.5,
"priceCurrency": "GBP",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembershipProduct": [
"https://api.example.com/membership-products/1",
"https://api.example.com/membership-products/2"
],
},
{
"name": "Private Member price",
"eligibleCustomerType": "http://openactive.io/ns#Member",
"eligibleMembershipProduct": [
"https://api.example.com/membership-products/3",
"https://api.example.com/membership-products/4"
],
}
],
Within the Customer Account API, it is possible to surface the MembershipProduct
s that are currently associated with a CustomerAccount
.
Add an additional property instanceOfMembershipProduct
to Entitlement
(the Entitlement
is an instance of the MembershipProduct
once it has been purchased).
{
"@context": "https://openactive.io/",
"@type": "CustomerAccount",
"@id": "https://eg.com/customer-accounts/fdc14503-275e-46d3-9922-45b986c9f9aa",
"identifier": "fdc14503-275e-46d3-9922-45b986c9f9aa",
"accountNumber": "CA00000123",
"customer": {
...
},
"hasHiddenEntitlements": false,
"entitlement": [
{
"@type": "Entitlement",
"validFrom": "2021-08-26T11:40:00+01:00",
"validUntil": "2022-09-25T10:45:33+00:00",
"instanceOfMembershipProduct": {
"@type": "MembershipProduct",
"@id": "https://api.example.com/membership-products/1",
"name": "Off-Peak Fitness",
"description": "Unlimited access to the gym, fitness classes, swimming pool and select facilities at off-peak times",
"membershipProductType": "https://openactive.io/MembershipSubscription"
}
}
],
...
}
Note the above proposals assume that the purchase of a subscription MembershipProduct
is achieved through deep-linking, as the Open Booking API does currently support direct debit or recurring payments.
Additionally it’s worth noting that if a particular Place
does not have any publicly visible opportunities (such as a private members club), then Place
and crucially Seller data will not be available unless the Booking System implements a Places feed.
As always, thoughts and feedback on all of the above very welcome!
My concern is that we're getting to the point where we need a relatively large amount of feeds which all serve a single purpose of reducing data in the main feeds, but with data that doesn't actually have a high rate of churn (e.g. Sellers, Memberships Places etc.). I'm wondering if it would be easier from a broker/on-boarding perspective to have a single-feed (called something like /metadata
) which returns all of this information:
{
"next": "https://example.com/oa/metadata?afterId=83376c5c-6749-4679-bba2-5b30ae390fd1&afterTimestamp=1234567899",
"items": [{
"state": "updated",
"kind": "Place",
"data": {...}
}, {
"state": "updated",
"kind": "Seller",
"data": {...}
}, {
"state": "updated",
"kind": "Membership",
"data": {...}
}]
}
This would be harder to implement from a booking system perspective (although I can't see that it would be a huge difficulty), but enables adding future items into this feed without requiring onboarding between the Broker & Booking System.
Which organisation(s) are proposing this issue?
Usecase
To enable membership options to be offered inline booking journey.
Please describe why your use case is not covered by the existing specification
Classes are not returned with a flag indicating which Memberships offer the class inclusive of the membership. Details of these Membership offers should be available elsewhere in the API.
What are you proposing should be changed in the specification?
Example
References
MemberShipFlow.pdf