Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session extension to indicate membership inclusive classes #45

Open
markthomasUprise opened this issue May 18, 2018 · 14 comments
Open

Session extension to indicate membership inclusive classes #45

markthomasUprise opened this issue May 18, 2018 · 14 comments
Labels
discussion Further information is requested out-of-scope-v1 Out of scope for version 1

Comments

@markthomasUprise
Copy link

markthomasUprise commented May 18, 2018

Which organisation(s) are proposing this issue?

  • UpriseVSI - Mark Thomas
  • LeisureCentre.com web booking journey for classes activities & membership

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?

  • Extension of GET '/Sessions'
  • Extend Offer to include optional Memberships
  • For each Membership list MembershipType

Example

"offer": [
    {
        "name": "Non-member price",
        "price": 4.5,
        "priceCurrency": "GBP"
    },
    {
        "name": "Member price",
        "price": 4.5,
        "priceCurrency": "GBP",
        "eligibleCustomerType": "http://openactive.io/ns#Member",
        "Membership": [
            {
                "MembershipTypeId": 0,
                ...
                "MembershipTypeId": n
            }
        ],
    },
    {
        "name": "Member price",
        "price": 3.5,
        "priceCurrency": "GBP",
        "eligibleCustomerType": "http://openactive.io/ns#Member",
        "Membership": [
            {
                "MembershipTypeId": 0,
                ...
                "MembershipTypeId": n
            }
        ],
    }
],

References

MemberShipFlow.pdf

@nickevansuk
Copy link
Contributor

nickevansuk commented Jun 8, 2018

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.)

@nickevansuk
Copy link
Contributor

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"
        }
    ]
  }
]

@peter-dolkens
Copy link

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"
        }
    ]
  }
]

@ldodds
Copy link
Contributor

ldodds commented Jun 19, 2018

As a general point of process, I think there's actually two aspects to this proposal:

  1. how do we describe member pricing and membership options within Offers. This is an extension/refinement to the modelling opportunity data specification.

  2. 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.

@nickevansuk
Copy link
Contributor

nickevansuk commented Sep 9, 2018

Noting here some overlap with #60, where memberships might be better sat inside organizer

Also noting that schema.org's ProgramMembership could be used or subclassed for this purpose.

@nickevansuk
Copy link
Contributor

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)

@nickevansuk
Copy link
Contributor

nickevansuk commented Sep 9, 2018

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 #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 openactive/implementation-tracker#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.

@nickevansuk
Copy link
Contributor

nickevansuk commented Sep 9, 2018

Also perhaps Membership should be named more generically to include "10 session pass" as well as just "monthly membership". So perhaps "AccessPass"?

@nickevansuk
Copy link
Contributor

nickevansuk commented Sep 9, 2018

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.

@nickevansuk
Copy link
Contributor

nickevansuk commented Sep 9, 2018

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 #60

@nickevansuk
Copy link
Contributor

To add further thoughts to this, the types of membership options seem to break out into two main categories:

  • Subscriptions - monthly subscription providing access to a site/sites or set of activities (e.g "Gym and Swim membership", "Off-peak membership")
  • Passes - one-off purchase that provides access to a sites/sites or activity/activities for a set a duration or number of visits/sessions (e.g. "1 day pass", "5 class pass", "1 month off-peak pass").

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):

  • Validity startTime and endTime: Allowed pass will be valid (usable) between startTime and endTime. Attempts to use a user payment option to make a booking outside of this interval will fail. E.g. "Off peak pass"
  • Single-use / Multi-use / Unlimited-use: "5 class pass" vs "5 day pass"
  • remainingUses: The limit of uses of a pass, e.g. "5 class pass" (could re-use remainingUses?)
  • maximumUses: The remaining uses of a pass e.g. "5 class pass, 3 uses remaining" (could re-use maximumUses?)

@ldodds ldodds added the out-of-scope-v1 Out of scope for version 1 label Nov 28, 2018
@nickevansuk nickevansuk added the discussion Further information is requested label Feb 13, 2020
@nickevansuk
Copy link
Contributor

Another two type of "membership" / booking constraints to consider:

  • Some opportunities are "business customers only", where e.g. businesses are given a special code to book, perhaps as part of an employee wellness programme.
  • Some opportunities, particularly on university campuses, are "student only", where only students are allowed to book on (and they need to provide valid student ID).

Both of the above constraints could be satisfied by an additional property on the "Offer".

@nickevansuk
Copy link
Contributor

nickevansuk commented Jul 11, 2022

Further considerations as we move towards a concrete proposal:

  • Memberships are bound to the Seller (otherwise they become "entitlements", see Customer Accounts API)
  • From the use cases above and more broadly (specifically considering leisure operators and tennis clubs), it appears that memberships may be applicable only to specific Places and/or opportunities relating to that Seller

This also mirrors what is described in Google Reserve: #45 (comment)

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.

Describing Memberships

Option 1

Embed memberships data in existing opportunity feeds

Advantages

  • No additional feeds required

Disadvantages

  • A very high volume of duplicate data is created, which is a burden on both the Booking Systems and Brokers

Option 2

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.

Advantages

  • Would reduce the volume of duplicate data in feeds (which warrants discussion separately)

Disadvantages

  • Would not scale to leisure operators that require a large number of memberships to be represented per-Seller
  • Requires a Sellers feed

Option 3

A separate memberships feed could be created, which references the Sellers

Advantages

  • Scales to meet the needs of leisure operators
  • Memberships are not duplicated

Disadvantages

  • Requires an extra feed

Option 4

Memberships could be embedded in Places, 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.

Click here to expand
{
  "@type": "Place",
  ...
  "eligibleMembershipProduct": [
    {
      "@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",
      "offers": [
        {
          "@type": "Offer",
          "price": 45,
          "priceCurrency": "GBP",
          "paymentFrequency": "http://openactive.io/ns#Monthly",
          "url": "https://bookingsystem.example.com/membership/purchase/deep/link/4"
        }
      ]
    },
    {
      "@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",
      "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"
        }
      ]
    },
    {
      "@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.",
      "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"
        }
      ]
    },
    {
      "@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.",
      "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"
        }
      ]
    },
    {
      "@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
      },
      "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"
        }
      ]
    }
  ]
}

Advantages

  • Scales to meet the needs of leisure operators
  • Simpler for Brokers to consume and display along with a Place (which is a common use case)
  • Simpler for Booking Systems in the simple case

Disadvantages

  • Requires a Places feed
  • Does not cater well for use cases where the membership is for the Seller and not the Place (e.g. a membership to a martial arts club that operates independently out of a leisure centre)
  • Duplicates membership data between Places (though this may not be an issue if membership data is not excessive)
  • Less obvious that Broker can dedupe and then advertise memberships at the Seller level (and potentially slightly more complex for them to do so), though perhaps simpler complexity than consuming an additional feed

Proposal

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 MembershipProducts 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.

  • When displaying information about the Seller, all MembershipProducts relating to the seller can be displayed
  • When displaying information about the Place, all MembershipProducts relating to the Place (via eligibleMembershipProduct) can be displayed
  • When displaying information about the opportunity, all MembershipProducts relating to the opportunity (via eligibleMembershipProduct) can be displayed

A 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.

Modelling notes

  • schema:hostingOrganization from schema:ProgramMembership has been used
  • A new enumeration of membershipProductType is proposed with values of "https://openactive.io/MembershipSubscription" and "https://openactive.io/MembershipPass" (to handle this requirement)
  • Given the potentially large number of memberships available, it may be desirable for a Broker to filter only those appropriate to the user. The restriction properties available for opportunities (ageRestriction and genderRestriction) are proposed to be applied to MembershipProduct for this purpose.
  • The term 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.)

Booking activities using membership pricing

Proposal

Using the patterns already included in the Customer Accounts API, the existing use of eligibleCustomerType beta, and referencing in the above discussion, Offers 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"
        ],
    }
],

Getting current memberships of an authenticated Customer Account

Within the Customer Account API, it is possible to surface the MembershipProducts that are currently associated with a CustomerAccount.

Proposal

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"
      }
    }
  ],
  ...
}

Other considerations

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!

@nathansalter
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Further information is requested out-of-scope-v1 Out of scope for version 1
Projects
None yet
Development

No branches or pull requests

5 participants