# Onboarding operations

Club-scoped onboarding query, special blocks, attachments, and validation.

Source: https://docs.champagne.dev-ukab.fr/en/graphql/onboarding

`onboardings(clubId: ID!)` returns every onboarding owned by a club. Use
`onboarding(clubId: ID!, id: ID!)` for one record. Each returns `id`, `clubId`,
`name`, `description`, `revision`, and ordered `questions`
plus `afterApproval` and `hosted` configurations. Hosted settings include `enabled`, `slug`,
`font`, and `primaryColor`. Each block includes `id`, `title`,
`type`, `required`, `options`, nullable `documentName`, nullable `documentData`, nullable
`personField` and `attributeKey` targets, the answer `format`, and, for identity verification and
booking blocks, `identityVerification` and `reservationCategoryId`. Questions and after-approval
steps share this shape.

```graphql
query ClubOnboardings($clubId: ID! = "51000000-0000-4000-8000-000000000051") {
  onboardings(clubId: $clubId) {
    id
    clubId
    name
    description
    revision
    questions {
      id
      title
      type
      required
      options
      documentName
      documentData
    }
    afterApproval {
      redirectUrl
      steps {
        id
        type
        documentName
      }
    }
  }
}
```

The example defaults to the seeded Ukab Clubs club and runs with empty variables (`{}`).
Override `clubId` with an ID from `clubDirectory` to inspect another club. An unavailable
club returns an empty list.

`hostedOnboarding(slug: String!, id: ID!)` is the public lookup used by
`https://<club-slug>.champagne.dev-ukab.fr/onboarding/<onboarding-id>`. The slug belongs to the club and is
configured once in club settings; `id` selects one of that club's published onboardings. Disabled,
unknown, or mismatched combinations return `null`.

`updateClubHostedSlug(clubId: ID!, slug: String)` sets that club-wide slug and returns the updated
`Club` (`id`, `name`, `hostedSlug`, ...). It requires `settings_manage` in the club. The slug is
trimmed and lowercased before validation and must be 3–63 lowercase letters, numbers, or inner
hyphens. The platform's own subdomains (`app`, `api`, `consumer`, `docs`, `mail`, `news`, `staging`,
`www`) are refused with `BAD_USER_INPUT`. `null` or a blank string removes it, which takes every
hosted onboarding of the club offline. Slugs are unique across clubs: a slug already used by
another club returns `BAD_USER_INPUT` ("This address is already used by another club.") and
leaves both clubs unchanged. An unknown club returns `BAD_USER_INPUT` ("This club is
unavailable."). The change is recorded in the club's activity history.

```graphql
mutation SetHostedSlug($clubId: ID!, $slug: String) {
  updateClubHostedSlug(clubId: $clubId, slug: $slug) {
    id
    hostedSlug
  }
}
```

`createOnboarding(input: CreateOnboardingInput!)` accepts a `clubId`, `name`, and
`description`. It creates a revision-one onboarding with required name and email-verification
blocks and returns `{ onboarding, error }`. The generated onboarding ID is a compact value such as
`onb_4f3a2b1c9d`; seeded journeys use readable IDs such as `common-members`.

`saveOnboarding(input: SaveOnboardingInput!)` accepts `id`, `clubId`, `name`,
`description`, the loaded `revision`, the complete ordered `questions` array, and
`afterApproval`, and `hosted`. Hosted settings retain per-onboarding enablement, font, and primary
color; the shared subdomain is not part of this input. It edits that onboarding atomically, increments its revision, and returns
`{ onboarding, error }`. A stale revision or unavailable club/onboarding returns
a readable error. For a club with the `billing` module turned off, a `PLAN_SELECTION` or
`PLAN_PAYMENT` step returns the error "Payments are turned off for this club.", and
`onboarding`, `onboardings` and `hostedOnboarding` leave those steps out; see
[Clubs with billing turned off](/en/graphql/platform#clubs-with-billing-turned-off).

```graphql
mutation SaveOnboarding($input: SaveOnboardingInput!) {
  saveOnboarding(input: $input) {
    onboarding {
      id
      revision
    }
    error
  }
}
```

An `OnboardingQuestionInput` supplies `id`, `title`, `type`, `required`, and
`options`. For ordinary choice questions, `options` contains labels. For plan selection,
it contains the plan IDs offered during onboarding; an empty list retains the legacy behavior of
offering every currently available club plan. Other special blocks use `required: true` and
`options: []`.

| Type                    | Rules                                                                                                                                                                     |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SHORT_ANSWER`          | Only in `questions`; empty options; optional `personField` or `attributeKey` target                                                                                       |
| `LONG_ANSWER`           | Only in `questions`; empty options; optional text `attributeKey` target                                                                                                   |
| `OPTIONS`               | Only in `questions`; 2–20 distinct choices (case-insensitive after trimming), or choices taken from its `personField` or `attributeKey` target                            |
| `PLAN_SELECTION`        | Only in `questions`; at most one; up to 100 distinct plan IDs in `options`                                                                                                |
| `PLAN_PAYMENT`          | Only in `afterApproval.steps`; at most one; requires plan selection                                                                                                       |
| `DOCUMENT_SIGNATURE`    | Only in `afterApproval.steps`; at most one; required PDF attachment                                                                                                       |
| `IDENTITY_VERIFICATION` | Either side; at most one overall; always required; optional `identityVerification` checks; refused when Stripe Identity is not configured                                 |
| `EMAIL_OTP`             | Only in `questions`; exactly one, required; empty options                                                                                                                 |
| `PHONE_OTP`             | Either side; at most one overall; always required; empty options; verifies a mobile number by text message and saves it as the person phone number                        |
| `PERSON_NAME`           | Only in `questions`; exactly one, required; empty options                                                                                                                 |
| `PERSON_ADDRESS`        | Either side; at most one overall; empty options; collects a structured `Address` (`line1`, `line2`, `city`, `postalCode`, `state`, `country`) saved as the person address |
| `RESERVATION_BOOKING`   | Either side; at most one overall; requires `reservationCategoryId` (see [Booking step](#booking-step)); `required` may be false                                           |

"Either side" blocks can be placed in `questions` (answered while the application is a draft) or
in `afterApproval.steps` (answered once accepted), but only once across both lists. Before approval,
`IDENTITY_VERIFICATION` and `RESERVATION_BOOKING` must come after the `PERSON_NAME` and
`EMAIL_OTP` blocks. Address and phone answered before approval are forwarded to the person on
acceptance; after approval they are saved on the person immediately. Breaking these rules returns
`BAD_USER_INPUT` with one of:

* "After approval can only contain plan payment, document signature, address, phone verification, identity verification and booking steps."
* "Plan payment and document signature belong in After approval."
* "Applicant address can appear only once."
* "Phone verification can appear only once and is always required."
* "Identity verification can appear only once."
* "Booking can appear only once."
* "Place identity verification and booking after the applicant name and email."

The `afterApproval` object contains an ordered `steps` array and an optional absolute HTTP(S)
`redirectUrl`. A custom frontend receives the application ID in that URL's `applicationId` query
parameter. When the URL is omitted, approval emails link to Champagne's consumer app.

Names allow 1–200 trimmed characters and descriptions 1–1000. Onboardings contain
1–50 application blocks with unique UUIDs. Onboarding IDs contain 3–32 lowercase letters,
numbers, underscores, or hyphens and begin with a letter. Block titles allow 1–500
trimmed characters and option labels 1–200. Invalid inputs yield
GraphQL `BAD_USER_INPUT` before any database write.

Hosted slugs use 3–63 lowercase letters, numbers, and hyphens, cannot begin or end with a hyphen,
and cannot use reserved Champagne service labels. Enabled slugs are globally unique. The font is
one of `MODERN`, `CLASSIC`, or `EDITORIAL`; the primary color is a six-digit hexadecimal color.

Supply both `documentName` and canonical base64 `documentData` for signature
blocks. Names allow up to 200 characters and must end in `.pdf`. The API checks
the PDF header and a 2 MiB decoded size per document; total document data is
limited to 4 MiB. Other block types reject attachments.

## Booking step [#booking-step]

A `RESERVATION_BOOKING` block lets an applicant book a visit, for example a discovery tour, either
while applying or once accepted, before becoming a member. `reservationCategoryId` names the
reservation category it books; other block types reject the field with `BAD_USER_INPUT`, and a
booking block without a valid UUID receives
`BAD_USER_INPUT: "Choose the reservation category of each booking step."`. These rules apply on
either side. When saving, the category must also be:

* a category of the same club; a category of another club or one that no longer exists returns
  `error: "Choose a reservation category of this club for each booking step."`;
* booked by time slot (`slotBooking`), otherwise
  `error: "Choose a reservation category booked by time slot."`;
* free: no resource with an `amount`, a `depositAmount` or a plan price above 0, otherwise
  `error: "Onboarding visits are free. Choose a category without prices."`. Onboarding never takes
  payment for a booking; charge for later visits through billing.

Mark the category [`onboardingOnly`](/en/graphql/reservations#reservation-categories) to keep it out
of member booking. A category used by a booking block on either side cannot be deleted while the
block remains: `deleteReservationCategory` returns
`error: "An onboarding booking step uses this category. Remove the step first."`. Applicants book
through the [application booking operations](/en/graphql/applications#booking-a-visit).

Question targets and answer formats are described in
[Member attributes](/en/graphql/person-attributes#save-onboarding-answers).

Application execution is handled by [application operations](/en/graphql/applications).
Payment and document signatures are available only after acceptance. Payment uses hosted Stripe
Checkout (see [application operations](/en/graphql/applications)); document signatures use Yousign's
hosted flow and a server-verified completion operation. Email verification sends a real message through the configured mail provider.

`IDENTITY_VERIFICATION` asks applicants to verify an ID document with
[Stripe Identity](/en/graphql/applications#identity-verification). Its
`identityVerification: IdentityVerificationSettingsInput` sets the checks; it is only accepted on
this block type and defaults to:

| Field              | Default | Rule                                                                                 |
| ------------------ | ------- | ------------------------------------------------------------------------------------ |
| `minimumAge`       | `18`    | Integer 0–99 compared with the document's date of birth; `0` disables the age check  |
| `requireSelfie`    | `true`  | Stripe compares a selfie with the document photo (`require_matching_selfie`)         |
| `requireNameMatch` | `true`  | Every word of the applicant's first and last name must appear in the document's name |

`identityVerificationAvailable(clubId: ID!): Boolean!` tells staff editors whether this server can
run Stripe Identity. When it is false, `saveOnboarding` refuses an onboarding that contains the
step with `BAD_USER_INPUT` "Identity verification with Stripe Identity is not configured on this
server." The query needs staff access to the club.

```graphql
query IdentityVerificationAvailable($clubId: ID!) {
  identityVerificationAvailable(clubId: $clubId)
}
```

`SPONSORS` adds a required sponsorship question answered with one-time sponsor codes
(see [sponsorship](/en/graphql/applications#sponsorship)). `sponsorCount` accepts 1–5 (default 2)
and is returned on questions; it is null for other block types. One sponsorship block is
allowed per onboarding. The configuration is copied into new applications.
