# Applications Immutable onboarding submissions, the consumer wizard, and back-office review. Source: https://docs.champagne.dev-ukab.fr/en/applications An application begins as an anonymous completion of a club onboarding, with no person or account required. The consumer creates a draft for a club and advances through one ordered block at a time. Its URL identifies the draft; reading or advancing it also requires its scoped application token. A new browser session can recover access by verifying the mailbox. Completing the last block submits the application. The back office lists applications for a club next to the action for editing its onboarding. Starting an application copies the onboarding revision, questions, options, and signature documents. Drafts and submitted applications therefore remain readable when an administrator edits the source onboarding. Answers are keyed by stable block IDs inside that snapshot. Every onboarding contains exactly one required **Applicant name** block and one required **Email verification** block. The entered name is shown in the back office's **Applicant** column. Before it is completed, the UI displays **Application in progress**. Payment and document-signature steps are explicit, required after-approval steps. Payment opens Stripe Checkout for the plan selected in the application and the club's connected merchant. The step advances only after Champagne receives the signed paid webhook and verifies that exact application Checkout session. On return from Checkout, payment confirmation uses the scoped application access token: applicants do not need to sign in as members before their account setup is complete. While the paid webhook is pending, the consumer checks every two seconds; an error stops automatic checks and offers a retry. Document signature uploads the snapshotted club PDF to Yousign, redirects the verified applicant to its hosted signing flow, and advances only after Champagne confirms the stored Yousign signer is signed. Email verification sends a random six-digit code that expires after ten minutes. Development messages are delivered to the local Mailpit inbox printed by `make dev` or `make worktree-up`. Submitted applications may be **Accepted** or **Rejected**. Both actions require confirmation in a dialog before any change or email is sent; rejection allows an optional internal reason. Acceptance atomically creates a person from the name, verified email, and collected address, copies ordinary answers into additional information, and links that person to the originating application. Acceptance and rejection send an email to the applicant. A rejection may include an optional reason, which appears on the application detail page and in its status history. The current rejection email does not include this internal reason. Every applicant advance and reviewer update checks the application revision, preventing two stale clients from silently overwriting one another. Set `YOUSIGN_API_KEY` in the API's ignored `.env.local` during local development and as a secret environment variable in hosted environments. Local development defaults to the sandbox URL in `config.mk`; override `YOUSIGN_API_URL` in `config.local.mk` for another endpoint. Set `YOUSIGN_API_URL=https://api.yousign.app/v3` in the hosted runtime for a production key. `CONSUMER_ORIGIN` supplies the exact return origin, and local `.localhost` origins are accepted for development. Yousign custom redirect URLs require an eligible paid plan. Sandbox requests use Yousign's supported embedded flow, which emits a completion event before Champagne verifies the signer and returns to the application; production requests use Yousign's hosted redirects. See [Application operations](/en/graphql/applications) for the complete API contract. ## Decision history [#decision-history] The application detail page includes **Status history**, ordered chronologically, with transition dates, action origins and the reason recorded for a rejection. A rejected application can be accepted directly after confirmation; accepted applications remain final. The former **Under review** step is no longer available. Its existing history entries are retained; applications still in that old state appear as **Submitted** and can be accepted or rejected. Staff decisions record the authenticated actor ID and name. Applicant access uses a scoped application token or the linked member session; older actor details may be absent. Older applications display an explicit incomplete-history notice; no dates or authors are reconstructed. See [Status transitions and history](/en/graphql/applications#status-transitions-and-history) for the allowed transitions, atomic persistence and API fields. ## Finding applications [#finding-applications] Use **Filters** beside the search field to select one or more statuses and an optional submission-date range (UTC). No selected status means all statuses. **Apply filters** updates the list; **Cancel** discards edits; **Reset** clears the draft filters and takes effect when applied. The button counts active filters. Filters and name/email search are combined on the server and stored in the page URL. The list shows up to 200 newest matches and asks you to narrow the search if that limit is reached. # Consumer app Apply to a club and sign in to a protected member account. Source: https://docs.champagne.dev-ukab.fr/en/consumer `consumer/` is the member-facing client; `web/` is the backoffice. Both use React, HeroUI, React Router, React Query, and Vite. The consumer showcases every customer-facing feature of the API: club onboardings, applicant flows, and an authenticated member account. ## Navigation [#navigation] The main navigation holds four tabs, each with its own URL. On desktop they sit in the header; on phones the header keeps only the brand, theme and language controls, and the tabs move to a fixed bottom bar with icons and short labels. | Tab | Route | Content | | ------------ | ----------------------- | ------------------------------------------------------------------------- | | Clubs | `/` | Club picker, club profile, every onboarding with its steps, and its plans | | Book online | `/book` | Online booking for visitors without an account | | Applications | `/applications/recover` | Find and resume an application; application and signature pages | | My account | `/account` | Sign in when signed out, otherwise the member area | The member area groups its pages into four sections. Each page keeps its own URL, and a section with several pages shows them as a segmented switch: | Section | Pages and routes | | ------------ | ----------------------------------------------------------------------------------------------- | | Overview | `/account` | | Reservations | `/account/reservations`, with details at `/account/reservations/` | | Billing | Membership (`/account/membership`), Credits (`/account/credits`), Invoices (`/account/billing`) | | Invitations | Club guests (`/account/guests`), Sponsorship (`/account/sponsorship`) | Sponsorship lets members generate, copy and revoke one-time sponsor codes for applicants. Messages (`/account/chats`) and Notifications (`/account/notifications`) form the **Inbox**, opened from the inbox button beside sign-out; its badge counts unread conversations and notifications. Guests only see Overview, Reservations and the Inbox; suspended, banned or disabled members lose Reservations and Invitations. Members whose bookings are blocked keep every section, but Reservations only lists their existing bookings with a notice instead of the booking planner. The **My profile** card on Overview lets members edit their name, phone, date of birth, gender and address; email changes still go through the club. Unavailable page URLs return to Overview. Password reset, set-password and guest-invitation pages belong to the My account tab. Source is grouped by feature under `consumer/src/`: `clubs/`, `applications/`, `account/`, `reservations/`, `auth/`, shared `layout/`, and `graphql/`. ## Anonymous applications [#anonymous-applications] The Clubs tab starts by choosing a club, then presents that club's profile, its named onboarding options with their ordered steps, and its membership plans; no person or account is required. When the club has a hosted slug and the onboarding enables hosting, the card links to its hosted page. The demo includes separate `/apply/common-members` and `/apply/founding-members` pages. The common path offers the member plan and requires two sponsor codes from members, while the privileged founding path offers Circle plans without sponsorship. Each start sends the selected copyable onboarding ID and creates a new draft from that exact onboarding. The browser moves to `/applications/`, with a scoped access token stored for that browser session. A new browser session can recover access by verifying the applicant email. Each step is saved through the API with optimistic revision checks. When the consumer is reached at an enabled club address such as `51-iena.champagne.dev-ukab.fr/onboarding/common-members`, it replaces the developer preview entry screen with that onboarding's standard application landing page. The club owns the subdomain, while each onboarding retains its enablement, font, and primary color. The shared club picker is not shown. Club subdomains do not expose reservation conversations. The shared consumer account implements the member side through authenticated reservation and conversation operations. The hosted subdomain remains available for onboarding and future club-owned experiences. Development routing sends both `consumer.` and direct club hosts such as `ukab-clubs.` to the consumer app. The required first-name/last-name block supplies the label later shown as **Applicant** in the back office. The required email block sends a random six-digit code through the configured mail provider and verifies it before the application can continue. Payment and document-signature steps remain visible and required after approval. A payment step shows the selected plan, opens the club merchant's Stripe Checkout, returns to the same application, and waits for the signed paid webhook before advancing. Cancellation, expiry, unavailable merchant configuration, and webhook delay remain visible and retryable. A signature step lets the applicant download the club-specific snapshotted PDF, opens Yousign's signing flow, and returns to the same application. Sandbox uses a dedicated embedded signing route so trial accounts can return automatically; production uses Yousign's hosted redirect flow. The browser event or return is only a prompt to verify the stored signer with Yousign; it cannot complete the step by itself. When Stripe sandbox billing is configured, the membership card loads `membershipOffers`, lets the person choose one of three annual IÉNA plans, and opens hosted Checkout through `createMembershipCheckout(personId, clubId, planId, requestId)`. The club-scoped offer response explicitly reports `ready` and `reason` when merchant activation or catalogue preparation is incomplete. The authenticated member interface handles pending/error/retry states and hides the CTA for people with Stripe-backed memberships. The membership card shows three stages: starting without a Stripe membership, paying in hosted Checkout, and receiving the signed webhook. After a success return, purchasing is hidden while `membershipCheckoutStatus` checks the exact `checkout_session_id`, person and club every two seconds in the foreground. Another existing membership cannot confirm this session. Once that session's subscription is projected, the account refreshes, polling stops and the card says **Webhook received — membership confirmed**, even if the return URL is still present. A failed refresh keeps existing data visible and is retried automatically. Redirect messages never activate membership; purchases are confirmed only by the signed paid webhook. Maxime Borde's pre-existing membership is seeded as described below. See [sandbox billing](/en/graphql/billing) for configuration, the API contract and shared Connect sandbox fixtures. Club context is functional isolation, enforced together with the authenticated member or staff identity. In the Membership tab, subscriptions and add-ons render as separate tiles. **My subscriptions** lists subscription memberships with the sandbox purchase flow; **Add-ons** lists the club's purchasable one-time extras from `addonOffers` with a per-plan **Buy** button backed by `createAddonCheckout` and `addonCheckoutStatus`, excluding already-owned plans; **My add-ons** lists owned add-on memberships. The add-on success return carries `kind=addon` so each tile polls only its own Checkout kind. The Reservations tab calls `myReservations(clubId)` and `myReservationWaitlist(clubId)` and displays the member's upcoming pending and confirmed bookings plus future waitlist entries and their current positions. The API derives the customer from the member session; the browser cannot request another person's reservation list. Selecting a booking opens its detail with edit and cancellation tools. Conversation-enabled requests also show a fixed-height chat. For those types (without sessions) the request form asks for no time: staff propose one, optionally as a free session, and the member accepts it from the same detail page. Only that acceptance books the reservation: a paid proposal becomes **Booked · payment due**, a free one is confirmed directly. Editing a pending request clears an earlier proposal. None of this is exposed as a `/chat/` route on the club subdomain. When a reservation category requires sessions, the request form lists future occurrences; members cannot submit an arbitrary time. The reservation editor keeps the occurrence dates read-only; resource selection remains available within the server-side session restrictions. A full capacity-limited occurrence remains selectable so the member can join its FIFO waitlist, see their position, or leave it. When a place opens, the first waiting member is booked automatically and notified by email; requesting the session directly also clears their waitlist entry. Flexible types keep editable local start/end fields, while server-side blocked periods prevent requests during closures or renovation. The Ukab demo seed publishes both `common-members` and `founding-members`, gives Alex Morgan three upcoming reservations, and enables conversation by default for **Private lessons**. Alex also has two private lesson requests awaiting a time: one without a proposal yet, and one with a free trial session proposed by the coach, ready to accept. Alex's seeded member credentials are `alex@example.com` and the standard development password. ## Member area [#member-area] The Overview tab summarizes the membership, the next reservation and outstanding invoices, each linking to its tab, followed by the profile, additional information and club. The profile ID comes from the member session, never from the URL. The visible account reloads automatically on tab focus. Errors remain visible alongside cached data if a refresh fails. Missing profiles, empty membership lists, and absent fields have explicit states. A person is shown as a member when a membership record exists; expiration dates are displayed separately, without inferring an active billing status. Without a fixture manifest, seed creates no Stripe memberships. A validated non-secret manifest under `.local/` restores **Maxime Borde** (`00000000-0000-4000-8000-000000000091`) with his paid connected Cercle + Sport membership, and selects the Ukab Clubs merchant. Its Price ID and paid-through date come from the bootstrap's verified sandbox invoice. The date is a snapshot, not a perpetual or automatically refreshed entitlement. Louis retains his contact details, extra fields, and club for testing a first purchase. Edited records and paid memberships survive restarts. A previous successful test payment therefore remains confirmed on subsequent visits; choose another person without a Stripe membership for another purchase. Seeding never resets or cancels a Stripe subscription; contradictory supplied fixture associations fail explicitly. Both apps label records without a Stripe Price ID as **non-Stripe memberships**. They are not missing webhook data. Open the consumer and back office URLs printed by the same `make worktree-up`: separate worktrees use separate databases, so paying in one does not update another. ## Local development [#local-development] Run `make setup` and `make dev`. Open the printed **Consumer** URL: `http://consumer.localhost:`. Both apps start and hot-reload together. `make logs` includes the consumer, and `make down` stops the stack while preserving data. In an isolated worktree, use `make worktree-up` and its printed consumer URL. It uses `consumer.` on the same allocated HTTP port. A worktree also prints its Mailpit inbox URL for inspecting OTP and application-decision messages. An older saved Compose snapshot is refreshed the next time the stack starts. With a sandbox API key in `.env`, `worktree-up` also manages the local Stripe listener and supplies its signing secret automatically. `worktree-down` stops the listener with that stack. Add `YOUSIGN_API_KEY` to the worktree's ignored `.env.local` to enable document signatures. Sandbox is used by default. The Compose-provided consumer origin is used for the Yousign return URL. ## API operations [#api-operations] The browser sends requests to relative `/api/graphql`, just as `web/` does. Development Caddy routes it to the local API. The independent deployment's Caddy forwards it to the runtime `CHAMPAGNE_ORIGIN`; browser CORS configuration is unnecessary. To extend the consumer: 1. Add the operation to `consumer/src/graphql/operations.graphql`. 2. Keep document inputs scoped to each frontend output in `scripts/codegen.ts`. 3. Run `make codegen`; never edit generated code manually. 4. Add transport in the feature's `consumer/src/graphql/.ts` module (built on `request.ts`) and React Query options in `consumer/src/graphql/queries.ts`, passing cancellation signals and surfacing errors. 5. Use the query from the relevant page and test its observable behavior. Keep consumer operations and UI independent of back-office internals. Member identity comes from Champagne's authenticated member session rather than URL parameters. ## Checks [#checks] `make fix`, `make check`, `make test`, and `make build` include the consumer. `make test-consumer` runs its tests alone, with `COVERAGE=1` for coverage. The existing Storybook remains associated with `web/`. ## Deployment [#deployment] `Dockerfile.consumer` builds a separate static Caddy image. Like `web/`, pushes to `main` deploy production and pushes to `staging` deploy staging after CI passes. Each environment deploys its own immutable consumer image digest alongside the API. | Environment | Consumer URL | Runtime `CHAMPAGNE_ORIGIN` | | ----------- | ------------------------------------------------ | --------------------------------------- | | Production | `https://consumer.champagne.dev-ukab.fr` | `https://champagne.dev-ukab.fr` | | Staging | `https://consumer.staging.champagne.dev-ukab.fr` | `https://staging.champagne.dev-ukab.fr` | Provision each environment's consumer container and domain, then configure `SCW_CONSUMER_CONTAINER_ID` and `CONSUMER_ORIGIN` in the matching GitHub environment before its first release. Both variables are required: missing or partial configuration fails the release rather than silently skipping the consumer. Deployment verifies the ready container's image digest and smoke-tests the public consumer URL before recording success. The consumer container has no database access or secrets. Its static page remains available when the API is unavailable; API requests preserve upstream errors. CI smoke tests run the image against the real disposable Champagne backend and check HTML, assets, deep links, 404s, and GraphQL forwarding. ## Sign in and account access [#sign-in-and-account-access] The My account tab shows the Welcome back form while signed out. It uses club names, email and password; `/login` redirects to `/account`. After sign-in, set-password or guest acceptance, the browser opens `/account`. An expired session returns to `/account?session=ended`. Development can display a seeded account chooser that fills the demo credentials. Production does not expose this chooser. The profile ID comes from the authenticated session, never from a trusted URL parameter. Forgot password asks for club and email; reset links expire after 30 minutes and can be used once. See [Authentication](/en/graphql/auth) for the API and session rules. ## Notifications [#notifications] **My account → Notifications** contains the member's inbox, unread count, read actions and linked reservation/account details. The **Preferences** tab controls in-app and email delivery separately for reservation changes, proposals, sponsored members joining, guests joining, reminders, waitlist updates, subscription renewal, session cancellations and coach messages. Both channels default to on. Reminder windows are 24 hours for confirmed reservations and 7 days before a Stripe subscription period ends. Waitlist availability invites the member to book; no automatic booking or payment occurs. See [Member notifications](/en/graphql/notifications) for the contract. ## Member booking flow [#member-booking-flow] The member Reservations tab starts with one activity selector containing every reservation type. Scheduled activities show a Monday–Sunday planning view in the club timezone. Selecting a session opens the integrated request form with its activity and occurrence preselected. Flexible activities show resource and date fields directly; conversation-based activities let the club propose the time. A sole resource is selected automatically. Upcoming reservations remain below this booking flow. Submission stays explicit and the existing membership, capacity, waitlist and payment rules remain unchanged. ## Member session isolation [#member-session-isolation] Signing in, choosing an initial password and accepting a guest invitation all cancel pending queries and clear the query and mutation caches before installing the new member token. Signing out and detecting an expired session use the same transition. Authenticated member query keys include an opaque session scope in addition to their existing club, person and resource parameters. Bearer tokens are not added to these keys. Prefix invalidation and polling remain unchanged. Public onboarding catalogs keep their existing keys. Responses started under a previous member token are discarded after a session change, including late mutation responses. Retry actions attached to errors from removed queries or mutations cannot execute under the new member session. # Drizzle and Effect 4 Automatic schema push, native Effect queries, and generated credentials. Source: https://docs.champagne.dev-ukab.fr/en/database The bootstrap tables live in the `public` namespace and are exported from `backend/src/db/schemas/index.ts` for Drizzle Kit. Outbound webhook scheduling lives in a separate `pgboss` schema managed by pg-boss at backend bootstrap, before requests are accepted. Drizzle Kit remains scoped to application tables in `public`. The database role needs schema/object creation privileges. The queue uses the same PG settings, password secret and verified CA as the application, with a separate pool of up to three connections per process. Budget 13 connections per API process: five `Database`, five separate `MailDatabase`, and three pg-boss connections, plus headroom for deployment overlap and operators. A repository-level `fromDrizzle` adapter captures the transaction's Effect context so business writes, `webhook_outbox` inserts and pg-boss jobs commit or roll back together on the reserved connection. Queue settlement and outbox updates are transactional too. Four scoped fibers per API process consume the shared queue; shutdown closes the pool. See [outbound webhooks](/en/webhooks) for retries, leases, scaling and operational SQL. Application tables include: * `person`: UUID ID, firstName, lastName, avatar, unique required email, gender, birthDate, structured address object, phoneNumber, required clubId, typed attributes and creation timestamp. * `person_attribute`: per-club attribute definitions with a unique key, label, fixed type and choice options. * `club`: UUID ID, required name, avatar, description, structured address object, globally unique hostedSlug, onboardingId, nullable merchantProfileId and creation timestamp. Multiple clubs can select the same merchant profile. * `onboarding`: short text ID, clubId, identifying name and description, hosted enablement, font and primary color, ordered questions, after-approval configuration, and revision. A club can publish multiple onboardings below its shared hosted slug. See [onboarding](/en/onboarding). * `reservation_category`: club-scoped type configuration with name, description, conversation flag, session requirement, and nullable default check-in/check-out minute offsets. * `reservation_resource`: named club resources owned by exactly one reservation category; names are unique case-insensitively within a club. * `reservation_session`: weekly UTC session definitions and one-off custom sessions, each with an optional member capacity. * `reservation_session_guest_person`, `reservation_session_guest_plan`: a session's optional guest list. Deleting the session, person or plan removes the entry. * `reservation_waitlist_entry`: FIFO member entries for full session occurrences, including the selected resource and materialized occurrence dates. * `reservation_block`: club/type-scoped unavailable time ranges with an optional reason. * `reservation`: club/type/customer booking, requested window (null for a pending or canceled request awaiting a staff proposal), nullable staff-proposed alternative window with its free flag, nullable session reference and check-in/check-out overrides, guests, status, creation timestamp, and exactly one resource. The resource must belong to the reservation's type; its displayed name is read from the resource, so renaming a resource renames it on every reservation. * `reservation_message`: ordered customer/professional messages for conversation-enabled reservations. * `reservation_chat_staff`: per-staff conversation participation, last-read time, and archive state. A row is created when that staff account first replies to the reservation chat. * `application`: a club-scoped immutable onboarding snapshot with applicant name, verified email, structured applicant address, expiring salted email challenge, ordered answers, progress, review status, optional rejection note, finalization-reminder schedule and count, timestamps, revision, and a nullable unique person reference populated only on acceptance. See [applications](/en/applications). * `plan`: UUID ID, required name, optional description, nonnegative integer amount, currency, monthly/yearly interval, subscription/addon type, active state, creation timestamp and the optional successor plan that replaced it for new subscribers after a split. * `merchant_profile`: local identity, sandbox/platform and connected account IDs, configuration, payment and payout readiness, and last-check timestamp. * `merchant_plan`: merchant + plan mapping to a connected Product and Price. * `plan_price`: every connected Price created for a plan, with its amount, currency and interval, so renewals on historical or subscriber-specific Prices can be validated. * `stripe_customer`: person + connected account mapping to a Customer, with compound uniqueness preventing contradictory person/Customer associations within an account. * `guest_invitation`: club-scoped guest email, 12/24/48-hour access duration, inviting member or staff account, hashed single-use token, link expiry and acceptance time. * `guest_access`: one accepted invitation's temporary access grant, including the guest person, club, inviter kind and nullable current inviter reference, start, expiry and optional revocation. The latest grant determines whether a person is a current or former guest. See [guests](/en/graphql/guests). * `checkout_attempt`: durable person, club, merchant/account, plan/Price, request and session references, state and creation timestamp. * `membership`: person, plan, club/account, subscription, subscription item, Price, paid-period timestamps, calendar expiration date, creation timestamp, and an optional scheduled replacement Price with the time it is applied. * `invoice_projection`: account + public invoice ID, owning club and the provider snapshot. It is a local copy of Stripe data and is deleted with its club. * `invoice_job`: account-qualified scan, invoice and webhook jobs with cursor and durable retry state. A person belongs to exactly one club for life and has many memberships, joined through `membership.personId`. New seeded and Stripe-backed memberships reference one plan through `membership.planId`; the column is nullable, so a membership can exist without a plan. Foreign keys enforce these relationships and are indexed. Every table that stores both a person and a club also checks that the person belongs to that club, through a foreign key on `(personId, clubId)` to `person (id, clubId)`. Deleting a club deletes its people, onboarding and applications; deleting a person deletes their memberships and clears the accepted application's person reference. A referenced plan is archived rather than deleted. Customers are account-qualified, not universal person attributes. Stripe remains the source of truth for Customers, subscriptions and invoices. Billing references (memberships, Checkout attempts, Customers) prevent deletion of their person, club or merchant. Birth and expiration dates are calendar dates (`YYYY-MM-DD`), without time zones. Avatars hold URLs or storage keys; gender is free-form text. Person, application and club addresses are nullable JSONB objects with `line1`, `line2`, `city`, `postalCode`, `state`, and `country` fields; a CHECK constraint on each table rejects non-object JSON values. Profile details and membership expiration are nullable. A null expiration means no date has been recorded; it does not imply an active or lifetime membership. Plan amounts are stored as nonnegative integer minor currency units. Currency uses a lowercase three-letter code, and interval is `month` or `year`. `plan.stripeId` is not a universal Stripe reference; runtime pricing resolves club → merchant → plan mapping. Editing price fields rotates the connected Stripe Price because Stripe prices are immutable, while deletion archives the Product/Price and removes the mapping. The seeded IÉNA offers are annual (€1,800, €4,500 and €6,300), with no additional tax introduced. Paid-webhook reconciliation is keyed by connected account + subscription, preserves the historical club, and extends only verified paid periods without regression on out-of-order events. The local plan catalog is available through the [`plans` GraphQL query](/en/graphql/plans). `attributes` is a required JSONB object, defaulting to `{}`, with a database check rejecting arrays and scalar values. It maps the club's `person_attribute` keys to typed JSON values (string, number or boolean); see [Member attributes](/en/graphql/person-attributes). Text columns limited to a fixed set of values store lowercase snake\_case values, enforced by a database check. When GraphQL exposes such a column with another spelling, the backend converts at the API boundary: `person_attribute.type` stores `text`, `number`, `date`, `boolean` and `choice` for `TEXT`…`CHOICE`; `onboarding.hostedFont` stores `modern`, `classic` and `editorial`; `housekeeping_checklist_item.category` stores `after_reservation` for `afterReservation`; and `housekeeping_report.condition` stores `out_of_service` for `outOfService`. JSONB documents such as onboarding questions and floor-plan elements keep their API spelling. Each email is stored once. `person_credential` holds only the member's password hash; member login, password reset and activation join `person` and match `lower(btrim(person.email))`, which the per-club unique index on `person` covers, so changing a person's email changes their login. `staff_user.email` is trimmed and lowercased on every write and unique through `lower(email)`. First and last names are required. There is no legacy name column or fallback. The administrator-only people query selects only ID, firstName, lastName, and email. Onboarding management and application reviews require club administrator access. Applicant operations require a scoped token; person reads require ownership or administrator access. Entity tables record a `createdAt` timestamp, defaulting to the insert time. Link tables, transient rate-limit and queue rows, and tables with their own event time (such as `activity.occurredAt` or `building_entry.enteredAt`) do not. GraphQL does not expose these creation timestamps unless a type documents them. Schema push manages every table exported from the schema index. Other tables in `public` are left untouched. Upgrading databases created by older versions is not supported: recreate the database instead. ## Automatic schema push [#automatic-schema-push] Every backend start runs **`drizzle-kit push` before listening**. Edit the Drizzle schema and restart the backend; there are no generated SQL files or migration folders to manage. Startup uses `--force` and automatically resolves unknown rename/create decisions as **create**, retrying with generated hints. There are no explicit rename hints. Destructive changes are approved without prompting: tables or columns can be dropped and data can be lost, including when a rename is treated as drop-and-create. This development policy also applies to deployed API startup; do not rely on it to preserve data. Automatic decisions are logged. Database errors, malformed CLI responses, repeated unresolved decisions, or exceeding the bounded retry limit still fail startup. Concurrent pushes to the same database, such as two API instances starting together, wait for each other through a PostgreSQL advisory lock instead of applying the same changes twice. `backend/src/bootstrap/seed.ts` contains idempotent demo seeding after schema push. It inserts 39 fictional profiles and the five requested UKAB teammates and three plans with stable IDs. All seeded people belong to Ukab Clubs; Vosges and Astel remain available. 27 people have accepted applications, and Ukab Clubs has draft, submitted and rejected candidates. Ukab Clubs contains Yanis Meziane, Julian Cario, Maxime Borde, Chayanne Godard and Hichame Benaddi, using their first name at `ukab.fr`. Existing email matches retain their IDs. Unrelated custom profiles are preserved. Application progress and profile edits survive restarts. It creates no Stripe membership without an explicit `STRIPE_FIXTURE_MANIFEST` file. A valid manifest restores Ukab Clubs' merchant and connected catalog, Maxime Borde's Customer, and his verified paid Cercle + Sport subscription period. Vosges and Astel initially have no merchant. Business edits and later merchant selections survive; conflicting fixture references fail explicitly. An unreadable or invalid supplied manifest fails startup. The seed performs no Stripe API calls. Production seeding is off by default; Terraform's `demo_seed=true` enables it. See [Stripe Connect](/en/stripe-connect) for the manifest delivery path. ## Native Effect queries [#native-effect-queries] `Database` uses `drizzle-orm/effect-postgres` and `@effect/sql-pg`. Queries are Effects, not Promise queries wrapped in Effect. One scoped layer owns the pool and releases it on shutdown. GraphQL propagates request cancellation. ```ts const people = Effect.gen(function* () { const db = yield* Database; return yield* db .select() .from(person) .orderBy(person.firstName, person.lastName, person.id) .limit(100); }); ``` Ordinary statements use PostgreSQL autocommit. Explicit `db.transaction(...)` remains available when multiple statements must succeed or fail together. ## Configuration and secrets [#configuration-and-secrets] | Location | Purpose | | --------------------------- | ------------------------------------------------------------------------------------------------------ | | `config.mk` | Non-secret development settings; override with ignored `config.local.mk`. | | `secrets/postgres_password` | Automatically generated persistent password, mounted as a Docker secret. | | `.env` | Optional externally issued secrets, such as `STRIPE_SECRET_KEY`; never generated database credentials. | `make setup`, `make dev`, and first worktree startup generate a cryptographically random database password if necessary. `secrets/` is private and excluded from Git and Docker builds. Subsequent starts preserve it; missing or changed existing credentials are rejected rather than silently rotating access to persisted data. Compose passes the generated file to PostgreSQL via `POSTGRES_PASSWORD_FILE` and to the backend via `PGPASSWORD_FILE`. Host, port, username, and database are wired automatically. You never construct a database URL or put a password in `.env`. Compose loads optional external API keys from `.env` only into the backend. Terraform generates the production password with `random_password`, supplies the private database endpoint and trusted CA, and injects the password through Scaleway secret environment variables. Production needs no local `.env` file. Review a Terraform plan before applying changes to existing infrastructure; adopting the generated password rotates the previous database credential. ## Versions [#versions] Drizzle ORM/Kit are pinned to `1.0.0-rc.5-5935859` and Effect packages to `4.0.0-rc.112`. This Drizzle candidate supports Effect's current `Schema.TaggedError` API without a compatibility patch or Effect 3 adapter. ## Member notifications [#member-notifications] `member_notification` stores recipient-scoped inbox entries and email delivery state. `notification_preference` stores per-club/member/category channel choices; absent rows mean both channels enabled. `notification_event` retains deduplication keys for scheduled/join milestones, including suppressed notifications. `session_cancellation` records canceled occurrence instants without removing recurring session definitions. Pending cancellation refunds are derived from canceled occurrence reservations with a payment intent and no refund ID. # Development Worktrees, testing, documentation, and production builds. Source: https://docs.champagne.dev-ukab.fr/en/development ## Parallel worktrees [#parallel-worktrees] ```sh make worktree-add WORKTREE_PATH="$PWD/.worktree/example" WORKTREE_BRANCH=feat/example ``` From the new worktree, run `make worktree-up`. Each worktree gets a unique `.localhost` hostname, port allocation, and Compose project. Stack identity is recorded under `.local/`, so shutdown cannot target a different checkout. * `make worktree-logs`: inspect this stack. * `make worktree-down`: stop it without deleting volumes. * `make worktree-clean`: explicitly delete this stack's volumes and identity. * `make worktree-remove WORKTREE_PATH="$PWD/.worktree/example"`: remove a clean, stopped worktree from a retained checkout. Do not run all-worktree stop or cleanup commands while other sessions are working. For Stripe Connect, use the development sandbox settings in `config.mk` and a key in the worktree's private `.env`. The worktree helper copies regular parent `.env` files privately at creation; never copy generated database credentials or listener secrets yourself. With Stripe configured, startup refreshes the fixture and starts the stack's own Connect listener. Worktrees share sandbox resources. See [Stripe Connect](/en/stripe-connect) for configuration and webhook contracts. ## Quality checks [#quality-checks] Backend imports follow `routes/GraphQL -> services -> repository -> db`. Shared contracts live in `domain/`, generated backend contracts in `domain/generated.ts`, and startup-only composition in `bootstrap/`. Repositories own SQL, transaction locking, and persisted retry state; services own use-case and provider orchestration. HTTP responses and the GraphQL schema are unchanged. Use `~/` imports across backend directories and direct `./` imports within one directory. Backend try/catch/finally is rejected in favor of typed Effect recovery and resource scopes. Runtime-exporting implementations need tests; domain, database infrastructure, generated code, and startup composition are exempt. Both frontends enforce `@/` imports across directories. API/GraphQL modules cannot import UI or state/hooks, hooks cannot import UI, and lower modules cannot import app entrypoints. Exported React components need companion tests and stories; exported hooks need tests. Story fixtures use the local GraphQL schema and reject mutations, so they need neither a running API nor credentials. `make storybook-build` builds both frontends. Run `make storybook` for the admin preview or `bun run --filter @champagne/consumer storybook --port 6007` for the consumer preview. Component render tests supplement existing interaction tests. Tailwind and CSS lint policies are unchanged. `make check` runs code generation drift checks, type checking, formatting, linting and unused-code detection. Tooling contract tests under `scripts/*.test.ts` are CI-only; do not run them locally. `make test` runs backend and frontend tests plus a disposable PostgreSQL integration suite. The database tests verify autocommit, rollback, seeding, and GraphQL persistence. CI additionally builds and smoke-tests the production Docker image against its own PostgreSQL instance. The visual E2E studio runs only on explicit local request, outside `make test` and CI. Launch it with `make e2e-studio` in a task worktree. ## Documentation [#documentation] ```sh make docs make docs-check ``` Fumadocs runs on Next.js for authoring and exports static HTML, assets, and a client-side search index. Content lives in `docs/content/docs/`; source and navigation configuration live in `docs/source.config.ts` and the collection's `meta.json`. `make docs-check` typechecks, builds, and validates internal links and heading anchors without crawling external sites. The **Help** links of the back office and member area open the guide on the build's `VITE_DOCS_ORIGIN`. `make dev` and `make worktree-up` run a docs service with hot reload at `http://docs.:`, the local default unless `DOCS_ORIGIN` is set in `config.mk` or `config.local.mk`; deployed images take the GitHub environment's `DOCS_ORIGIN`, and fall back to the production documentation without it. `Dockerfile.docs` serves the export with Caddy at [docs.champagne.dev-ukab.fr](https://docs.champagne.dev-ukab.fr). Terraform owns its separate Scaleway container, DNS, and custom-domain binding. Main-branch CI/CD builds and deploys both the app and docs when the documentation container ID is configured. No Fumadocs account, API key, or Node.js runtime is required on the documentation container. ## Status page [#status-page] The documentation site serves the status page at `/en/status` and `/fr/status`, with an Atom feed at `status/feed.xml`. It lives on the documentation container so it stays reachable when the app or the API is down. The page checks the API from the reader's browser every minute through the documentation's `/api/graphql` proxy. Incidents are declared in `docs/lib/incidents.ts`; pushing the change to `main` publishes it with the next documentation deploy. ```ts export const incidents: readonly Incident[] = [ { id: "2026-11-09-api-outage", title: { en: "API errors", fr: "Erreurs de l'API" }, impact: "major", services: ["api", "memberSites"], updates: [ { at: "2026-11-09T14:05:00Z", phase: "investigating", message: { en: "Requests to the API fail.", fr: "Les requêtes vers l'API échouent." }, }, ], }, ]; ``` Append an update each time the situation changes, oldest first, through `investigating`, `identified`, `monitoring` and `resolved`: an incident stays open until its last update is `resolved`. `impact` is `degraded`, `partial` or `major`, and `services` lists the affected rows among `backOffice`, `memberSites`, `api` and `docs`. The `id` is the incident's anchor and feed entry, so keep it stable. ## Production image [#production-image] ```sh docker build -t champagne:local . # Terraform supplies production runtime connection settings and secrets. ``` Caddy serves the built frontend and React Router fallback on port 8080, and proxies `/api/*` to Elysia on internal port 3000. The backend serves GraphQL only. A supervisor shuts down both processes together. Terraform supplies production connection settings and generated credentials. Startup runs `drizzle-kit push`; no SQL files are needed. See [Database](/en/database). ## Frontend data and navigation [#frontend-data-and-navigation] React Query executes the generated people operation, owns caching and request cancellation, and exposes loading/error/refresh state to the HeroUI table. Fresh results are reused for 30 seconds. React Router redirects `/` to the first section the signed-in staff member can open and shows a not-found page for unknown routes. Caddy serves the HTML entrypoint for browser deep links while missing assets and unknown API routes stay 404s. The codegen configuration is `scripts/codegen.ts`. Project-specific testing and observability guidance is in `robot-docs/testing.md` and `robot-docs/observability.md`. ## Markdown and LLM access [#markdown-and-llm-access] Every documentation page offers **Copy Markdown** and an **Open** menu. The menu links to the page's GitHub source, its Markdown export, and supported AI tools. The Markdown actions use the static export, including the page's title, description, and canonical source URL. Cards, steps, and callouts are rendered as readable Markdown rather than copied HTML or JSX. * [llms.txt](/llms.txt): a generated index linking to the Markdown version of each page, followed by the French user guide. * [llms-full.txt](/llms-full.txt): all published documentation in one plain-text file. * [Introduction Markdown](/markdown/en/index.md): the homepage's Markdown export. Nested pages use `/markdown//.md`. The exports are generated from the same public content collection during the static build and served by Caddy; they do not require a Node.js server, API key, or an AI provider. `make docs-check` verifies the index links and full-text coverage alongside regular page links. ## Storybook [#storybook] The admin and consumer previews include component stories with local fixtures; they need no backend or database. `make storybook` uses port 6006 with `--exact-port` and fails if it is occupied. Coordinate with the owning session rather than interrupting it. The consumer command is listed under Quality checks. `make storybook-build` produces both `web/storybook-static/` and `consumer/storybook-static/` and runs in CI. Storybook is not publicly deployed. # Introduction A minimal Bun, GraphQL, React, and HeroUI starter. Source: https://docs.champagne.dev-ukab.fr/en Champagne provides one working example from the browser to the backend. There are no accounts, queues, or unrelated business features. One Person schema connects PostgreSQL to a HeroUI table through GraphQL. [Run the starter](https://docs.champagne.dev-ukab.fr/en/quickstart) Install dependencies and start the isolated local stack. [Explore GraphQL](https://docs.champagne.dev-ukab.fr/en/graphql) Query people and extend the generated contract. ## What's included [#whats-included] * Elysia and GraphQL Yoga on Bun, with public people selection, person details, and club directory queries. * Native Drizzle and Effect 4 PostgreSQL with autocommit by default. * React Query for GraphQL caching, cancellation, and refresh. * React Router and a HeroUI v3 table with loading, empty, error, and refresh states. * Caddy for static files, SPA routing fallback, and API proxying. * Generated GraphQL resolver and operation types. * Unit and component tests, linting, type checking, and production image checks. * This Fumadocs documentation site. The frontend imports Tailwind and HeroUI styles only. There are no custom CSS rules, theme overrides, or inline styles. # Onboarding A club's applicant onboarding, blocks, editing, and immutable application snapshots. Source: https://docs.champagne.dev-ukab.fr/en/onboarding Each club can have multiple onboardings for different audiences. Choose a club in the back-office switcher, open **Applications**, then select the **Onboardings** tab. The list shows each onboarding's name, description, and copyable short ID. Staff can add an onboarding and edit its identity and blocks. New clubs receive a common-members onboarding automatically. ## Champagne-hosted application site [#champagne-hosted-application-site] An onboarding can be published as a ready-to-use applicant site when a club does not want to build its own form. In the onboarding editor, enable **Champagne-hosted application**, choose a globally unique address label such as `51-iena`, then select a font and primary brand color. The resulting public address is `https://51-iena.champagne.dev-ukab.fr` (staging: `https://51-iena.staging.champagne.dev-ukab.fr`). Address labels are deliberately separate from both the onboarding name and its implementation ID. They use one DNS-safe label of 3–63 lowercase letters, numbers, and hyphens; leading or trailing hyphens and reserved Champagne service labels are rejected. This keeps a public address stable when staff rename an onboarding while still allowing an explicit address change. Disabling publication removes public resolution but retains the selected address and branding for later reuse. Every onboarding contains exactly one required **Applicant name** block and one required **Email verification** block. They cannot be removed or made optional. The entered first and last names identify the application in the back office and are used to create a person when the application is accepted. An optional **Applicant address** block can be added once; it collects a structured address (`line1`, `line2`, `city`, `postalCode`, `state`, `country`) whose value is forwarded to the person record on acceptance, or saved on it at once when asked after approval. ## Blocks and editing [#blocks-and-editing] An onboarding has an ordered list of 1–50 blocks. Ordinary questions have a stable UUID, title, required-answer flag, and one of three types: * **Short answer**: a single-line response. * **Long answer**: a multiline response. * **Options**: one choice from 2–20 distinct options. The editor also offers **Applicant name**, **Applicant address**, **Plan selection**, **Plan payment**, **Document signature**, and **Email verification** blocks. Email verification, plan selection, plan payment, and applicant address may each appear once. Payment requires a plan selection earlier in the list. Signature blocks may appear multiple times and each requires a PDF attachment before saving. Steps after approval are asked once the application is accepted. Plan payment and document signature belong there only. Applicant address, phone verification, identity verification and visit booking can be placed either before or after approval, once in the whole onboarding; before approval, identity verification and booking come after the applicant name and email blocks. PDF attachments are limited to 2 MiB each and 4 MiB across the onboarding. Save persists the whole onboarding atomically. Failed saves preserve pending edits; leaving with unsaved changes warns the administrator. Optimistic revisions reject stale saves instead of overwriting another edit. ## Database and API [#database-and-api] `public.onboarding` stores `id`, `clubId`, `name`, `description`, hosted-site publication and branding settings, ordered JSONB `questions`, `afterApproval`, and `revision`. Multiple rows can belong to one club; `club.onboardingId` keeps the automatically created common-members onboarding reference. Startup installs an idempotent PostgreSQL trigger and backfills clubs missing that initial onboarding. GraphQL exposes `onboardings(clubId)`, `onboarding(clubId, id)`, `hostedOnboarding(slug)`, `createOnboarding(input)`, and `saveOnboarding(input)`. Reads and writes keep explicit club scope; application starts instead use the short onboarding ID so consumer apps can route different audiences to different experiences. ## Application snapshots [#application-snapshots] Starting an application with `startApplication(onboardingId)` copies the selected onboarding's revision, ordered blocks, options, and signature documents. Later onboarding edits cannot reinterpret an in-progress or historical application. ## Sponsorship [#sponsorship] Add **Sponsorship** and set **Number of sponsors** (1–5, default 2). Applicants enter that many sponsor codes, each generated by a different active member from the **Sponsorship** tab of their account. A code works once and is checked when the step is saved. The application dossier lists the sponsors, and accepting the application records them on the new member's profile. The final admission remains a staff decision. Changing the count does not change existing applications. # Quickstart Start Champagne with an isolated PostgreSQL database. Source: https://docs.champagne.dev-ukab.fr/en/quickstart ## Requirements [#requirements] Install Bun 1.4.x, Node.js 24 LTS, Make, Git, and Docker with Compose. ### Install dependencies [#install-dependencies]From the repository root:```sh make setup ``` ### Start development [#start-development]```sh make dev ```Open the printed app URL. The people table loads demonstration profiles from PostgreSQL through GraphQL. Visible lists refresh automatically every 30 seconds and when the tab regains focus. The stack starts PostgreSQL, the API, the back office, the member app (`consumer.localhost`), the Mailpit inbox (`mail.localhost`), Waggle (`waggle.localhost`), Caddy, and a one-off dependency installer. `drizzle-kit push` and idempotent demo seeding run before the API listens. Generated passwords live in `secrets/` and are mounted as Docker secrets; no manual database URL or `.env` credentials are needed. ### Check your changes [#check-your-changes]```sh make fix make check make test make build ``` Stop the current stack with `make down`. This preserves its database and dependency volumes. See [Development](/en/development) for isolated worktrees and docs commands. The clubs are Vosges, Astel and Ukab Clubs, with three annual IÉNA plans. Without an explicitly configured fixture manifest there are no seeded Stripe memberships. To enable connected merchants, Finance and real sandbox Checkout, including accepted-application Plan payment steps, follow [Stripe Connect](/en/stripe-connect). Application payment remains unavailable until the selected club's merchant and catalogue are ready. # Stripe Connect Dev and deployment sandbox merchants, bootstrap fixtures, isolated listeners and deployment. Source: https://docs.champagne.dev-ukab.fr/en/stripe-connect ## Finance and Consumer [#finance-and-consumer] Open Settings at `/settings/payments?club=`. A club can select a registered merchant or create one through Stripe Accounts v2, complete activation, refresh its capabilities and prepare its catalog. The same merchant can be selected by several clubs. Incomplete activation is visible; Checkout requires payments readiness and a valid catalog. Payout readiness is displayed separately. IÉNA has three annual offers at €1,800, €4,500 and €6,300, without additional tax. Payments are direct charges in the connected account, with no Champagne commission. Each person has a Customer association for that account. A Checkout attempt fixes the historical club, account, plan and Price. The Consumer return includes `checkout_session_id` and waits for the subscription's paid projection; only the signed Connect `invoice.paid` webhook grants or extends the membership. An accepted application with a Plan payment step uses the same connected merchant, selected plan, hosted Checkout and signed-webhook confirmation before onboarding can continue. Finance reads PostgreSQL projections. Each associated merchant account is scanned once, including shared merchants, with separate durable cursors and jobs. Invoice ownership comes from subscription metadata and local historical associations, not account sharing or the person's current club. Full credits use `credit_amount` (customer balance), without a bank refund or email. Club administrators authenticate before accessing finance; member checkout operations verify ownership. See the [invoice](/en/graphql/invoices) and [billing](/en/graphql/billing) contracts. ## Dev and deployment sandboxes [#dev-and-deployment-sandboxes] Local development uses Champagne Dev Sandbox (`acct_1UIssNI56SIBRnby`); staging and production retain Champagne Prod Sandbox (`acct_1UIYvTEk8EhumAbT`) and its existing Ukab Clubs fixture. Runtime refuses live keys and events. The pinned SDK/API version is `2026-08-26.dahlia`. Worktrees isolate files, databases, listeners and signing secrets; they do not introduce Stripe resource namespaces within Dev. Stripe's built-in Test mode remains separate and unchanged. The shared demo merchant is Ukab Clubs, `contact@ukab.fr`, a French individual fixture. It has no Stripe dashboard; the platform collects its requirements and is responsible for fees and losses. This deterministic platform-managed model is restricted to the metadata-marked sandbox fixture. Merchants created through the application still use a full Stripe dashboard with Stripe collecting requirements and bearing losses. The club ID is `51000000-0000-4000-8000-000000000051`. Vosges and Astel initially have no merchant. Later business edits and merchant selections survive. ## Local configuration and bootstrap [#local-configuration-and-bootstrap] 1. Run `make dev` in a fresh checkout. On first interactive startup, paste the Dev sandbox's `sk_test_` key when prompted; the startup verifies `acct_1UIssNI56SIBRnby` and saves the key to an ignored local `.env`. It also creates its own Connect listener and signing secret. 2. For `make worktree-up`, provision the Dev sandbox key in that worktree's own `.env` or inject it into the command environment. `STRIPE_PLATFORM_ACCOUNT_ID` defaults to the Dev sandbox in `config.mk`; use `config.local.mk` only when intentionally targeting a different sandbox. `STRIPE_ENVIRONMENT` must be `sandbox`. 3. Set the sandbox account's default webhook API version to `2026-08-26.dahlia`. 4. Run `make worktree-up`. Compose derives both app origins and starts an isolated Connect listener. The listener uses `--forward-connect-to http://api:3000/api/webhooks/stripe` and the `checkout.session.completed` plus invoice, credit-note and invoice-payment event set. `checkout.session.completed` fulfills one-time add-on purchases; the invoice events project subscription periods. It does not forward platform events. The pinned CLI has no arbitrary `listen` API-version flag, so readiness verifies the advertised account-default version. Do not use `--latest`. Startup output containing the signing secret stays in the listener container; the private runtime environment files are not printed or copied from another checkout. `make dev` and `make worktree-up` automatically refresh the fixture and restart the API with its validated manifest. To verify idempotence manually, run bootstrap twice through that API's environment: ```sh bun --no-env-file scripts/dev-compose.ts exec -T -w /app api env -u STRIPE_FIXTURE_MANIFEST bun --no-env-file scripts/stripe-catalog.ts bun --no-env-file scripts/dev-compose.ts exec -T -w /app api env -u STRIPE_FIXTURE_MANIFEST bun --no-env-file scripts/stripe-catalog.ts ``` The script verifies the platform and sandbox, finds or creates the merchant, attaches its French test bank account and waits for Stripe's asynchronous capability activation. It then prepares the three Products/Prices and verifies Maxime Borde's connected Customer, Cercle + Sport subscription and paid €6,300 invoice. Partial runs are recoverable; ambiguous matches fail rather than selecting arbitrarily. Only a fully validated fixture is published atomically to `.local/stripe-connect-manifest.json`. No host Stripe login or hosted onboarding is required. For a custom external bootstrap manifest, explicitly configure seed restoration: ```make # config.local.mk STRIPE_PLATFORM_ACCOUNT_ID = acct_REPLACE_WITH_SANDBOX_PLATFORM STRIPE_FIXTURE_MANIFEST = .local/stripe-connect-manifest.json ``` `make worktree-up` copies the specified nonsecret file to `.local/worktree-stripe-manifest.json` and sets runtime `STRIPE_FIXTURE_MANIFEST=/app/.local/worktree-stripe-manifest.json`. An absolute source path can reuse an explicitly selected manifest elsewhere; the source is not modified. If replacing an already configured manifest's contents, run `worktree-up`, then `bun --no-env-file scripts/dev-compose.ts restart api` to rerun the seed. Ordinary local startup uses the freshly validated fixture automatically; without a key, it skips Stripe fixtures. A supplied unreadable, invalid or contradictory custom manifest fails startup. The seed makes no Stripe API calls and preserves edited business records. The manifest supports bootstrap and seeding; registered merchants continue working without it. Finance-created merchants do not rewrite it. ## Identity verification webhooks [#identity-verification-webhooks] [Identity verification steps](/en/graphql/applications#identity-verification) create Stripe Identity sessions on the platform account. Create another snapshot event destination in the **same sandbox platform**: * URL: `https:///api/webhooks/stripe/identity` (POST). * Source: **Your account**, not Connected accounts. * Secret: this destination's own `whsec_...` in `STRIPE_IDENTITY_WEBHOOK_SECRET`. * Events: `identity.verification_session.processing`, `identity.verification_session.verified`, `identity.verification_session.requires_input` and `identity.verification_session.canceled`. Create a restricted key with the **Identity verification results** and **Recent detailed verification results** read permissions and store it as `STRIPE_IDENTITY_RESULTS_KEY`; only such a key can read the date of birth used by minimum-age checks. Identity must also be activated for the account in the Dashboard. Local stacks forward these events with `stripe listen --forward-to` and write the listener secret as `STRIPE_IDENTITY_WEBHOOK_SECRET`; add `STRIPE_IDENTITY_RESULTS_KEY` to the worktree's own `.env`. ## Merchant status webhooks [#merchant-status-webhooks] Accounts v2 uses **thin notifications**, not v1 `account.updated` snapshots. Keep the invoice endpoint unchanged. Create another event destination in the **same sandbox platform**: * URL: `https:///api/webhooks/stripe/accounts` (POST). * Source: **Your account**, API `events_from: ["@self"]`, **not Connected accounts** (`@accounts`). * Payload: **Thin** (`event_payload: "thin"`). The SDK retrieves versioned events and accounts using the pinned `2026-08-26.dahlia` API; notification payloads are unversioned. * Secret: this destination's own `whsec_...` in `STRIPE_ACCOUNTS_WEBHOOK_SECRET`. * Subscribe to these exact event names: ```text v2.core.account.updated v2.core.account[configuration.merchant].updated v2.core.account[configuration.merchant].capability_status_updated v2.core.account[requirements].updated v2.core.account[identity].updated v2.core.account[defaults].updated ``` The handler verifies the raw-body signature with Stripe's asynchronous thin-notification parser, checks sandbox/platform context, verifies the API key's platform, and fetches the v2 event with its `Stripe-Context`. Only a locally registered account matching the configured sandbox/platform is re-retrieved, with merchant configuration, defaults, identity and requirements included. The account ID, country/entity type, sandbox mode, dashboard and fee/loss responsibilities are validated before atomically persisting `configurationStatus`, `paymentsEnabled`, `payoutsEnabled` and `checkedAt`. Payment readiness still means active card payments; payouts are independent. This never creates accounts, changes Stripe data, prepares catalogs or alters club selections. Concurrent webhook reconciliations lock the local merchant before retrieving its latest state. Duplicate and out-of-order deliveries re-read current Stripe state instead of replaying old event values; readiness is idempotent and `checkedAt` advances on successful checks. There is no event deduplication ledger or background queue for these small reconciliations. HTTP 200 follows the database commit; provider/database failures return 500 for Stripe retries. Invalid signatures return 400. Unsupported event types, live/foreign-context notifications, platform-account events and unknown local merchants are acknowledged without state changes. Incompatible retrieved accounts fail validation and retain the last stored state; closure/deletion and organization-wide destinations are not supported by this handler. Platform-scoped events can omit `context` in the notification and return `context: null` from the event retrieval API. Both mean the authenticated platform's own scope, not a foreign account; no `Stripe-Context` header is sent for these events. An explicit context must still match the configured platform and agree between the notification and retrieved event. Without the optional secret, this endpoint returns 503; existing invoice webhooks and manual merchant refresh continue working. Existing deployments therefore do not need a new secret until they enable automatic merchant updates. A missed event (including one delivered before a new merchant was saved locally) is recovered by automatic synchronization while Settings is open. The selected merchant is synchronized on opening, on tab focus, and every minute while visible. Settings polls the stored GraphQL merchant list every 3 seconds while any merchant's payment or payout capability is pending, otherwise every 30 seconds, and every 10 seconds after a query error. Polling pauses in background tabs; mounting and returning focus always refresh. Cached merchant details remain visible on failure with a retry error. Polling does **not** call Stripe. Both Stripe onboarding return and refresh URLs now target `/settings/payments?club=`; neither a redirect nor its query parameters mark a merchant ready. An expired link can be renewed with **Complete Stripe activation**. Initial loading and manual Stripe refresh show a centered spinner. Background polling keeps the merchant readable and shows **Checking for updates…** beside the last-checked timestamp; it never blocks activation while waiting for Stripe. The idle polling label describes the browser's checks, not webhook delivery health. **Last checked** advances only when the backend successfully retrieves Stripe state, not on every browser poll. Query failures retain the cached merchant and show **Automatic updates interrupted** with a retry action. ### Local thin listener [#local-thin-listener] Keep the worktree's existing automatic invoice listener running. Start a **second** Stripe CLI listener authenticated to the same sandbox platform (replace the URL for another worktree): ```sh stripe listen --thin-events 'v2.core.account.updated,v2.core.account[configuration.merchant].updated,v2.core.account[configuration.merchant].capability_status_updated,v2.core.account[requirements].updated,v2.core.account[identity].updated,v2.core.account[defaults].updated' \ --forward-thin-to http://settings-merchants-f8fde1ca0c6f.localhost:33910/api/webhooks/stripe/accounts ``` Use `--forward-thin-to`, **not** `--forward-thin-connect-to`. Put this new listener's signing secret in the current worktree's own `.env` as `STRIPE_ACCOUNTS_WEBHOOK_SECRET`; do not copy another checkout's credentials. Run `make worktree-up` to inject it into the private billing runtime environment and recreate the API if needed. The helper preserves the separate invoice listener secret. The thin listener is operator-managed and must remain running while testing; if its secret changes, update the setting and rerun `make worktree-up`. For the primary checkout (`make dev`), forward the second listener to `http://localhost:/api/webhooks/stripe/accounts`, using the port printed by `make dev`. Store its secret as `STRIPE_ACCOUNTS_WEBHOOK_SECRET` in the ignored `.env.local` (mode `0600`), then rerun `make dev` to recreate the API. Keep the invoice listener and its `STRIPE_WEBHOOK_SECRET` unchanged: an invoice-only listener cannot update merchant readiness. Verify the thin listener reports HTTP 200; a missing secret returns 503 and a reconciliation failure returns 500. Merely seeing the page poll does not prove Stripe delivery is configured. Finish activation or change a capability in Stripe's sandbox, then verify the event delivery is 200 and Settings updates without pressing refresh. Replay a delivery and verify no extra local merchant appears. No real Stripe mutations are required by automated tests: they use signed fixtures, the real SDK with an isolated mock HTTP transport, and a disposable PostgreSQL database. Official references: [webhooks](https://docs.stripe.com/webhooks), [Connect event scopes](https://docs.stripe.com/connect/webhooks?accounts-namespace=v2), and [thin event names](https://docs.stripe.com/api/v2/core/events/event-types). ## Staging and production delivery [#staging-and-production-delivery] The existing `infra/scaleway` module handles both environments with separate state. Configure: | Terraform input | Runtime result | | -------------------------------- | --------------------------------------------------------------------------- | | `stripe_environment = "sandbox"` | `STRIPE_ENVIRONMENT` | | `stripe_platform_account_id` | `STRIPE_PLATFORM_ACCOUNT_ID`, same platform in both environments | | `stripe_secret_key` | Secret `STRIPE_SECRET_KEY`, sandbox only | | `stripe_webhook_secret` | Secret `STRIPE_WEBHOOK_SECRET`, specific to this deployment's endpoint | | `stripe_accounts_webhook_secret` | Optional secret `STRIPE_ACCOUNTS_WEBHOOK_SECRET`, separate thin destination | | `app_domain`, `consumer_domain` | HTTPS `WEB_ORIGIN`, `CONSUMER_ORIGIN` | | `demo_seed = true` | `DEMO_SEED=true` | | `stripe_fixture_manifest_path` | Explicit local bootstrap JSON file read by Terraform | The provider supports environment and command injection, not arbitrary config-file mounts. Terraform encodes the nonsecret file as `STRIPE_FIXTURE_MANIFEST_JSON`. Its command wrapper (`infra/scaleway/start-runtime.sh`) atomically writes `/tmp/champagne-stripe-connect-manifest.json`, unsets the transport variable and executes the existing production supervisor. The backend receives `STRIPE_FIXTURE_MANIFEST` pointing to that file. This happens before schema push and seeding, on every cold start, without an image rebuild, bucket or new secret-management infrastructure. Use the same validated manifest for staging and production. Empty `stripe_fixture_manifest_path` means no fixture file or fixture seed. Supplying one requires Stripe and `demo_seed=true`; Terraform checks its format, sandbox and platform, and the backend validates the full manifest. Infrastructure configuration requires the normal Terraform plan/apply; image-only CD preserves it. Register a persistent sandbox **connected-account** snapshot webhook endpoint for each deployment: `https:///api/webhooks/stripe`, API version **`2026-08-26.dahlia`**. Subscribe to `invoice.created`, `invoice.finalized`, `invoice.updated`, `invoice.deleted`, `invoice.paid`, `invoice.payment_failed`, `invoice.payment_succeeded`, `invoice.voided`, `invoice.marked_uncollectible`, `credit_note.created`, `credit_note.updated`, `credit_note.voided` and `invoice_payment.paid`. Supply its own signing secret, not a local `stripe listen` secret. The application verifies signatures over the raw body, requires `event.account`, rejects live, acknowledges foreign events and keeps transient failures retryable. ## Verification [#verification] After bootstrap twice and seed, open Ukab Clubs' Finance page and verify its merchant, catalog, Maxime's paid membership and projected invoice. Complete another eligible person's Consumer Checkout with Stripe's `4242 4242 4242 4242` test card; the return must wait for webhook projection. Verify invoice details and a full customer-balance credit. Select the same merchant for a second club and verify invoice separation, then exercise creation and activation of another merchant. # Terraform provider Manage a club's configuration as code with the champagne Terraform provider. Source: https://docs.champagne.dev-ukab.fr/en/terraform Clubs that run several environments keep their configuration in Terraform and promote it from one environment to the next. The `champagne` provider manages the configuration of one club through the public [GraphQL API](/en/graphql): its settings, its plans, its reservation catalog with opening hours, recurring sessions, staff in charge, floor plans and housekeeping checklists, the attributes and tags it keeps on people, its webhook endpoint and its newsletter sender. It never manages people or activity (members, applications, reservations, invoices, who carries a tag). Terraform is the source of truth for what it manages: a change made in the back office shows up as drift at the next `terraform plan` and is reverted by the next `terraform apply`. ## Authenticate [#authenticate] A club administrator creates an [API token](/en/graphql/api-tokens) in the back office, under **Settings**, **Integrations**, **API tokens**, and grants it **Read and write** on each area the configuration manages. The token names the club: the provider takes no club id, and one token never reaches another club. | Area in the back office | Resources and data sources | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Club settings | `champagne_club_settings`, `data.champagne_club` (read is enough for the data source) | | Plans | `champagne_plan` | | Reservation catalog | `champagne_reservation_category`, `champagne_reservation_category_links`, `champagne_reservation_resource`, `champagne_reservation_session`, `data.champagne_staff_member` | | Floor plans | `champagne_floor_plan` | | Housekeeping | `champagne_housekeeping_checklist` | | Attributes and tags | `champagne_person_attribute`, `champagne_person_tag` | | Webhooks | `champagne_webhook` | | Newsletter sender | `champagne_newsletter_sender` | A resource whose area the token lacks fails with `This API token lacks the permission for this operation.` ```hcl terraform { required_providers { champagne = { source = "ukab-labs/champagne" } } } provider "champagne" { endpoint = "https://club.example.com/api/graphql" token = var.champagne_token } ``` | Argument | Environment variable | Description | | ---------- | -------------------- | ------------------------------------------------------------ | | `endpoint` | `CHAMPAGNE_ENDPOINT` | URL of the GraphQL API of the environment. | | `token` | `CHAMPAGNE_TOKEN` | Club API token with `write` on the managed areas. Sensitive. | Use one state per environment: each environment has its own endpoint, its own token and its own ids. A blocked, expired or deleted token, or a token of a suspended club, is refused when the provider starts. Every resource below is managed with the token of its club, except `champagne_club` and `champagne_space`, which take a [platform token](/en/graphql/api-tokens#platform-tokens). ## Resources [#resources] Every resource is importable with `terraform import
`. ### champagne\_club\_settings [#champagne_club_settings] The settings of the club, one resource per club. ```hcl resource "champagne_club_settings" "club" { description = "A club for content creators." timezone = "Europe/Paris" hosted_slug = "astel" address = { line1 = "1 rue de la Paix" postal_code = "75002" city = "Paris" country = "France" } deposit_refund_days = 7 no_show_sanctions = { reset_after_days = 90 rules = [ { no_shows = 2, action = "warn" }, { no_shows = 4, action = "suspend", suspension_days = 14 }, ] } } ``` | Argument | Left out | Notes | | ------------------------------------ | ---------------------- | --------------------------------------------------------------------- | | `description` | Cleared | At most 2,000 characters. | | `hosted_slug` | Cleared | 3 to 63 lowercase letters, digits or hyphens; unique across clubs. | | `address` | Cleared | `line1`, `postal_code`, `city`, `country`, optional `line2`, `state`. | | `timezone` | Keeps the club's value | IANA timezone. | | `reservation_guests_require_account` | Keeps the club's value | Boolean. | | `deposit_refund_days` | Keeps the club's value | 0 to 365. | | `credits` | Not managed | `name`, `per_euro`, `low_balance_threshold`; needs billing enabled. | | `no_show_sanctions` | Not managed | `reset_after_days` and up to 10 `rules`. | The club name is not a setting: only platform administrators rename a club. Destroying the resource removes it from the state and leaves the club's settings as they are. The import id is ignored, since the club is the one of the token. ### champagne\_plan [#champagne_plan] A membership plan, an add-on or a credit pack. Plans create Stripe products and prices: the club needs billing enabled and an active Stripe merchant. ```hcl resource "champagne_plan" "member" { name = "Member" amount = 180000 # cents interval = "year" } resource "champagne_plan" "locker" { name = "Locker" type = "addon" amount = 1500 interval = "month" } ``` Stripe prices are immutable, so a change is applied in one of three ways: | Argument | Change | | ------------------------------- | ---------------------------------------------------------------------------- | | `name`, `description` | Updated in place. | | `credit_amount` | Updated in place. | | `amount` of an add-on or a pack | Updated in place, with a new Stripe price. | | `amount` of a subscription | **Splits the plan**, which keeps its subscribers. | | `type`, `currency`, `interval` | **Deletes the plan and creates another one**; refused while members hold it. | A subscription changing price is split, like in the back office. Terraform shows an update in place whose `id` is known after apply: the resource then follows a new plan at the new price, offered from now on. The previous plan is kept as replaced, reserved to the members who hold it, at the price they pay; Terraform no longer tracks it, and destroying the resource leaves it to them. Changing `type`, `currency` or `interval` cannot be a split: Terraform deletes the plan and creates another one. The API refuses to delete a plan members still hold, so the apply fails with `Members still hold this plan. End or move their memberships before deleting it.` and nothing changes. The same refusal protects `terraform destroy`. `currency` defaults to `eur`, `interval` to `year` and `type` to `subscription`. `stripe_product_id` is exported. ### champagne\_reservation\_category [#champagne_reservation_category] A category of things members book. ```hcl resource "champagne_reservation_category" "visit" { name = "Discovery visit" onboarding_only = true waitlist_enabled = false slot_booking = true default_duration_minutes = 60 slot_interval_minutes = 60 min_notice_minutes = 120 booking_window_days = 30 } ``` `name` is unique in the club, ignoring case. The flags `conversation_required`, `create_ticket`, `requires_session`, `visualization_enabled`, `housekeeping_enabled`, `customer_sheets_enabled`, `external_booking_enabled`, `onboarding_only` and `slot_booking` default to `false`, `waitlist_enabled` to `true`, `slot_interval_minutes` to `30` and `currency` to `eur`. `default_duration_minutes`, `booking_window_days` and `min_notice_minutes` mean "no limit" when left out. See [booking rules](/en/graphql/reservations#booking-rules-and-time-slots) for what each setting does. A category holding reservations cannot be destroyed. `opening_hours` lists when slots can be booked, as windows of one day in the club's timezone; left out, the category has no opening hours: ```hcl resource "champagne_reservation_category" "restaurant" { name = "Restaurant" slot_booking = true default_duration_minutes = 120 opening_hours = [ { weekday = 2, start_minutes = 720, end_minutes = 870 }, # Tuesday 12:00-14:30 { weekday = 2, start_minutes = 1140, end_minutes = 1380 }, # Tuesday 19:00-23:00 ] } ``` `weekday` runs from 0 (Sunday) to 6 (Saturday). Windows of one day must not overlap. ### champagne\_reservation\_category\_links [#champagne_reservation_category_links] The categories a reservation of one category can be moved to, such as the two rooms of a restaurant. Links point one way; they are a resource of their own so two categories can link to each other. ```hcl resource "champagne_reservation_category_links" "restaurant" { category_id = champagne_reservation_category.restaurant.id linked_category_ids = [champagne_reservation_category.rooftop.id] } resource "champagne_reservation_category_links" "rooftop" { category_id = champagne_reservation_category.rooftop.id linked_category_ids = [champagne_reservation_category.restaurant.id] } ``` Declare at most one per category, with up to 50 other categories of the club. Destroying it removes the links. The import id is the category id. ### champagne\_reservation\_resource [#champagne_reservation_resource] One bookable thing of a category: a room, a table, a coach. ```hcl resource "champagne_reservation_resource" "podcast" { category_id = champagne_reservation_category.studios.id name = "Podcast studio" amount = 2500 # cents per price_unit price_unit = "hour" max_occupants = 4 } ``` `name` is unique in its category. `amount` defaults to `0`, `price_unit` (`once`, `hour`, `day` or `night`) to `once`, `max_occupants` to `1` and `external_bookable` to `false`. `deposit_amount`, `default_check_in_minutes`, `default_check_out_minutes`, `cancellation_notice_days` and `external_amount` are cleared when left out. Changing `category_id` replaces the resource. A resource holding reservations cannot be destroyed. Categories managed by Terraform leave their resources to this resource: adding, renaming or removing a resource never rewrites the category. `staff_user_ids` assigns the staff in charge of the resource, such as its coach. Staff accounts are people: they are invited from the back office and their ids differ between environments, so look them up by email with `champagne_staff_member`: ```hcl data "champagne_staff_member" "julien" { email = "julien@example.com" } resource "champagne_reservation_resource" "tennis" { category_id = champagne_reservation_category.lessons.id name = "Tennis with Julien" staff_user_ids = [data.champagne_staff_member.julien.id] } ``` Left out, the assignment is not managed; an empty set removes everyone. The API refuses to remove a staff member whose upcoming bookings would be left without anyone in charge. ### champagne\_reservation\_session [#champagne_reservation_session] A recurring session of a category with `requires_session`: a class members book a seat in. ```hcl resource "champagne_reservation_session" "pilates" { category_id = champagne_reservation_category.classes.id resource_id = champagne_reservation_resource.studio.id title = "Pilates" weekday = 1 # Monday start_minutes = 900 # 15:00 duration_minutes = 60 capacity = 8 } ``` `kind` is `weekly` (the default, with a `weekday`) or `daily` (without one). `capacity` is left out for unlimited sessions. `reservation_required = false` describes a session members attend without booking; it takes no `capacity`. `guest_plan_ids` restricts the session to the members of some plans. The API cannot edit a recurring session in place, so **any change replaces the session**: Terraform deletes it and creates another one. A session with bookings or a waitlist cannot be deleted, and therefore cannot be changed until they are cancelled. Dated one-off sessions are events rather than configuration: create them in the back office. The import id is `/`. ### champagne\_floor\_plan [#champagne_floor_plan] The floor plan of a category with `visualization_enabled`: its spaces and the elements drawn in them. Members pick the resource they book by selecting its element. ```hcl resource "champagne_floor_plan" "pool" { category_id = champagne_reservation_category.pool.id spaces = [{ id = "7b000000-0000-4000-8000-000000000001" name = "Outdoor pool" surface = "terrace" palette = "lagoon" }] elements = [ { id = "7b000000-0000-4000-8000-000000000101" space_id = "7b000000-0000-4000-8000-000000000001" kind = "swimmingPool" label = "Pool" x = 100 y = 80 width = 400 height = 200 locked = true }, { id = "7b000000-0000-4000-8000-000000000102" space_id = "7b000000-0000-4000-8000-000000000001" kind = "rectangle" label = champagne_reservation_resource.lounger.name resource_id = champagne_reservation_resource.lounger.id x = 120 y = 320 width = 60 height = 120 rotation = 90 }, ] } ``` The ids of spaces and elements are UUIDs written in the configuration, so they are the same in every environment. A plan has 1 to 12 `spaces` (`surface` is `terrace`, `interior` or `garden`; `palette` and `color` are optional) and at most 200 `elements`, drawn in order. | Element argument | Notes | | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `id`, `space_id`, `kind`, `label` | Required. `kind` is a shape such as `rectangle`, `ellipse`, `pottedPlant`, `parasol`. | | `x`, `y`, `width`, `height` | Required, in plan units. | | `rotation`, `locked` | Default to `0` and `false`. | | `resource_id` | Resource of the category booked through this element; each resource is placed once. | | `object_id` | Custom object of the category's library. Objects carry artwork made in the back office. | | `color`, `stroke_color`, `stroke_width`, `outline_only`, `label_visible`, `keep_aspect_ratio` | Optional appearance. | | `chairs`, `chair_positions`, `points`, `bookable` | Optional geometry of tables, polygons and decoration. | The API names an element placing a resource after that resource: set its `label` to the resource's `name`, as above, or the next plan reports a change. Such an element is a `rectangle` or an `ellipse`. `bookable = true` without a `resource_id` is refused, because the API would create a resource Terraform does not manage. The plan is saved over the revision currently stored: if someone edits it in the back office during an apply, the apply fails and can be run again. Destroying the resource removes every element and keeps the spaces, since a stored plan always has one. The import id is the category id. ### champagne\_housekeeping\_checklist [#champagne_housekeeping_checklist] The housekeeping checklist of one resource, in a category with `housekeeping_enabled`. ```hcl resource "champagne_housekeeping_checklist" "garden" { category_id = champagne_reservation_category.suites.id resource_id = champagne_reservation_resource.garden.id items = [ { category = "daily", label = "Air the room" }, { category = "daily", label = "Restock towels" }, { category = "after_reservation", label = "Change the sheets" }, ] } ``` `items` holds at most 100 items in display order. `category` is `daily` (every day) or `after_reservation` (each departure); labels are distinct within a category. An item is identified by its category and label: reordering keeps the ticks staff recorded, rewording an item replaces it and drops them. Destroying the resource empties the checklist. Reading a checklist needs the club's housekeeping module. The import id is `/`. ### champagne\_webhook [#champagne_webhook] One webhook endpoint of the club. A club has up to 10 of them, each with its own URL, events and signing secret. See [webhooks](/en/webhooks) for the events and their signature. ```hcl resource "champagne_webhook" "website" { url = "https://hooks.example.com/champagne" enabled_events = ["reservation.created", "application.status_changed"] } resource "champagne_webhook" "accounting" { url = "https://accounting.example.com/hooks" enabled_events = ["invoice.issued", "invoice.paid", "invoice.credited"] enabled = false } output "website_signing_secret" { value = champagne_webhook.website.signing_secret sensitive = true } ``` `url` is a public HTTPS URL, unique in the club. `enabled_events` lists the event types to send; left out, the endpoint keeps its selection, which starts with every event. `enabled` defaults to `true`; set it to `false` to pause deliveries, knowing that events that happen meanwhile are not sent later. `signing_secret` is exported and sensitive: it is read when the resource is created or imported, then kept in the state, so store the state accordingly. After rotating a secret in the back office, remove the resource from the state and import it again to read the new one. Destroying the resource deletes the endpoint and its secret. Import with the endpoint id. ### champagne\_newsletter\_sender [#champagne_newsletter_sender] The address the club sends its newsletters from, one resource per club. It needs newsletter sending configured on the platform. ```hcl resource "champagne_newsletter_sender" "club" { domain = "news.example.com" sender_name = "Astel" } output "newsletter_dns_records" { value = champagne_newsletter_sender.club.dns_records } ``` `local_part` defaults to `newsletter`. `sender_email`, `status` (`pending`, `verified` or `failed`) and `dns_records` (`type`, `name`, `value`) are exported: publish the records in the DNS zone of the domain, with Terraform or by hand, and the status becomes `verified` once they are found. The API cannot remove a sender: destroying the resource only removes it from the state. The import id is ignored. ### champagne\_person\_attribute [#champagne_person_attribute] A custom field the club keeps on each person. ```hcl resource "champagne_person_attribute" "language" { key = "preferred_language" label = "Preferred language" type = "choice" options = ["French", "English", "German"] } ``` `key` is the stable identifier of the attribute, unique in the club: a lowercase letter followed by up to 39 lowercase letters, digits or underscores. `type` is `text` (the default), `number`, `date`, `boolean` or `choice`; `options` lists the 2 to 50 choices of a `choice` attribute and is left out otherwise. `label` and `options` are updated in place. Changing `key` or `type` **replaces the attribute**, which erases the values people hold for it. An attribute used by an onboarding question cannot be destroyed. See [member attributes](/en/graphql/person-attributes). ### champagne\_person\_tag [#champagne_person_tag] A staff-only label grouping people of the club. ```hcl resource "champagne_person_tag" "founder" { label = "Founding member" color = "amber" } ``` `label` is unique in the club, ignoring case, and holds at most 24 characters. `color` defaults to `gray`; the palette is listed with [member tags](/en/graphql/person-tags). Terraform manages the tag, never who carries it. Destroying a tag removes it from everyone carrying it. ### champagne\_club [#champagne_club] A club of a customer space. It is the one resource managed with a **platform** token, created by a platform admin in the **Platform** section under **API tokens**: configure a provider alias for it, next to the provider of each club. ```hcl provider "champagne" { alias = "platform" token = var.champagne_platform_token } data "champagne_space" "customer" { provider = champagne.platform name = "NJJ Hospitality" } resource "champagne_club" "astel" { provider = champagne.platform space_id = data.champagne_space.customer.id name = "Astel" timezone = "Europe/Paris" disabled_features = ["billing"] } ``` `name` is unique in its space, ignoring case, and starts with a capital letter. `timezone` defaults to `Europe/Paris`. `disabled_features` turns modules off for the club: `reservations`, `availability`, `customer_chats`, `housekeeping`, `newsletters`, `applications`, `billing`. `hosted_slug` can be set here or in `champagne_club_settings`, not in both. `status` is exported. Changing `space_id` replaces the club. The platform never deletes a club: **destroying the resource archives it**, which closes it to its members and staff and keeps its data. An archived club keeps its name, so a club destroyed this way cannot be created again under the same name in that space; restore it from the back office and import it instead. A new club has no API token: a club administrator creates one in the back office before the club's own configuration can be applied. Spaces and their owners are created in the back office too. ## Data sources [#data-sources] ### champagne\_space [#champagne_space] A customer space found by `name` (ignoring case), with a platform token: `id`. ### champagne\_club [#champagne_club-1] The club of the token: `id`, `name`, `description`, `timezone`, `hosted_slug` and `billing_enabled`. ```hcl data "champagne_club" "current" {} ``` ### champagne\_staff\_member [#champagne_staff_member] A staff member of the club found by `email` (ignoring case): `id` and `name`. Fails when nobody in the club's team has this email. ## Example [#example] `terraform-provider/examples/astel` in the repository configures the Astel club like the demo seed does: its settings, four categories of spaces with their resources, the discovery visit booked during onboarding and the attribute staff fill in for each creator. ## Develop the provider [#develop-the-provider] The provider lives in `terraform-provider/` and only talks to the GraphQL API. Go and Terraform run in Docker, behind `make` targets: | Target | Does | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `make provider-build` | Builds the provider into `terraform-provider/bin`. | | `make provider-fix` | Formats the Go code and the examples. | | `make provider-check` | Checks formatting, runs `go vet` and the tests against an in-memory API, validates the example. | | `make provider-testacc` | Runs the same tests against the running stack, in a club created for the run. | | `make provider-example` | Applies the example to a club created for the run, then requires an empty plan. | | `make provider-try` | Runs Terraform on a copy of the example in `.local/provider-try` with the token in `CHAMPAGNE_TOKEN`: shows the plan and asks before applying. `TF=plan` and `TF=destroy` run those instead. | `provider-testacc`, `provider-example` and `provider-try` need a stack started with `make worktree-up` or `make dev`: they sign in with the seeded demo accounts to create a club and its token. The plan tests need Stripe and are skipped unless run against a club with an active merchant: ```bash make provider-testacc CLUB_ID=51000000-0000-4000-8000-000000000051 CHAMPAGNE_TEST_PLANS=1 ARGS="-run Plan" ``` The `provider` job of the CI runs `provider-check`, starts the Compose stack, then runs `provider-example` and `provider-testacc` against it, so every pull request applies the example to a real API and requires its second plan to be empty. # Outbound webhooks Receive committed Champagne lifecycle changes in an external backend. Source: https://docs.champagne.dev-ukab.fr/en/webhooks Champagne sends **outbound** HTTP POSTs to each club's configured backend endpoints. This is separate from the **incoming** Stripe endpoints at `/api/webhooks/stripe`. The receiving backend can decide whether to send email, SMS or push notifications; Champagne webhooks do not imply that any of those channels were used. Champagne also has an independent member inbox and email notification worker; see [Member notifications](/en/graphql/notifications). ## Configure [#configure] An administrator opens **Settings → Integrations → Webhook endpoints** to add up to 10 endpoints per club. Each endpoint has its own URL, event selection and signing secret, so separate systems can each receive only what they need. The multi-select options and descriptions come from a global catalog table. Adding an endpoint generates its secret to store in the receiving backend; the endpoint menu reveals or resets it, sends a test, pauses or deletes the endpoint. Pause an endpoint or select no events to queue none. Only public HTTPS DNS names are accepted. Do not point a real club at webhook.site: embedded workers may deliver actual club events there. Redirects are not followed; Stripe's incoming webhook secret is unrelated. Outbound delivery runs inside the API process locally and on Scaleway. Bootstrap initializes pg-boss's `pgboss` schema and `outbound-webhooks` queue before accepting requests, then starts four application-scoped delivery fibers polling every 500 ms. There is no separate worker image, container, CRON trigger, worker token or internal drain/test endpoint. The queue uses the same `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, password secret (`PGPASSWORD_FILE` locally, `PGPASSWORD` in production) and `DATABASE_CA_PEM` as the application. Its separate pool is capped at three connections per API process. The runtime database role must be able to create the `pgboss` schema and its objects. Shutdown interrupts the scoped pollers and closes the queue pool. Keep at least one API replica running (`min_scale >= 1`) to process deliveries and retries without incoming traffic. Each additional API process adds four pollers and up to three queue connections alongside five `Database` and five separate `MailDatabase` connections. Size database capacity for **13 connections per API process**, with headroom for deployment overlap and operational clients. Scaling the API also scales webhook consumers; pg-boss coordinates claims across replicas. If every API process is stopped, jobs remain in PostgreSQL and processing resumes on startup, subject to queue retention. Releases deploy the API image by digest and use the database-backed GraphQL readiness probe. Because the listener starts after queue initialization and fiber startup, this gates bootstrap; it does not prove receiver availability or ongoing queue progress. The disposable production-image check also submits a safe job referencing no business event, waits for the embedded consumer to complete it without HTTP delivery, and checks that queue sessions are absent after process exit. Post-exit absence does not prove the scoped finalizer ran: process termination also closes sockets. Backend integration coverage separately verifies live scoped disposal while the process remains alive. For a receiver-only smoke test, an authorized club administrator uses **Test webhook** or [`sendClubTestWebhook`](/en/graphql/webhooks). This sends a synthetic signed event to the saved URL without member data; it bypasses the queue and does not test background retries. Use an isolated test club and a controlled receiver when validating the complete lifecycle. ### Existing-environment cutover [#existing-environment-cutover] This development change has no compatibility migration or automatic replay of legacy pending events. Choose the pending-event policy explicitly before deploying a version whose startup pushes the schema: 1. Pause business writes that could create outbound events. If pending legacy events must be retained, let the old worker drain them using the old schema, and verify no pending events or active claims remain. If this is disposable development data, explicitly approve discarding it instead; record that those notifications will not be delivered. Do not silently drop a backlog or assume pg-boss will adopt it. 2. Quiesce the old CRON trigger and any manual drain callers. Wait for all in-flight worker requests and claims to finish; stop the old worker before any schema push. Keep writes paused so the old producer cannot recreate a backlog between draining and cutover. Never run the legacy worker against the new schema. 3. Prepare the API's PG credentials/CA, schema privileges and capacity for 13 connections per process. Set `min_scale >= 1` and an integer `max_scale >= min_scale` through a reviewed configuration change. Deploy the combined API implementation only after the legacy consumer is quiescent. 4. Verify GraphQL readiness, the embedded startup record and fresh queue snapshots. With a controlled receiver and isolated test club, verify a committed event is delivered and retry behavior works; the direct **Test webhook** mutation alone does not prove queue consumption. Resume business writes once the embedded lifecycle is verified. 5. Remove the old worker container, trigger, IAM key/policy/application and worker/synthetic credentials through a separately reviewed Terraform plan/apply against the reconciled existing state. Remove obsolete tfvars and protected GitHub environment settings after cutover. Ordinary image CD does **not** delete infrastructure or revoke credentials. Preserve unrelated API secrets, database and DNS. All steps require the environment owner's approval; this source change performs no production operation. ## Transactional queue [#transactional-queue] Events enter `webhook_outbox` and pg-boss in the same business transaction, **once per enabled endpoint of the club that selected that event type**. Each endpoint's copy is a separate delivery with its own job, retries and outcome, so a failing receiver never delays the others. The repository uses pg-boss's `fromDrizzle` transaction adapter, capturing the current Effect context so its SQL executes on the reserved transaction connection. A rollback removes both the business change and queued job. Job data contains the outbox delivery ID, which is also the job ID. Every delivery of one event carries the same envelope `id`. API requests never wait for the receiving backend. ### Embedded delivery lifecycle [#embedded-delivery-lifecycle] Business changes and queue jobs share one commit; each running API process consumes the same queue. ```mermaid flowchart LR Request[Business request] --> Transaction[Drizzle Effect transaction] Transaction --> Business[Business rows] Transaction --> Outbox[One outbox delivery per subscribed endpoint] Transaction --> Adapter[pg-boss transaction adapter] Adapter --> Queue[PostgreSQL pg-boss queue] Queue --> Consumers[Four scoped fibers per API process] Consumers --> Receiver[Signed HTTPS delivery] Receiver --> Settlement[Transactional job and outbox settlement] Settlement --> Retry[Retry or complete] Retry --> Queue ``` A `2xx` response acknowledges delivery. Timeouts, redirects and non-2xx responses use pg-boss's retry policy: **20 retries**, exponential backoff starting at **2 seconds**, capped at **1 hour**, and a **60-second lease**. Retry timing also depends on polling and expiration recovery; there is no immediate-delivery or cross-event ordering guarantee. After retry exhaustion, the job is `failed` and no longer retries automatically. Queue retention and completed/failed job deletion are configured for 30 days; delivered outbox records are pruned after 30 days. Events during an unconfigured or paused period are not queued or backfilled. Deliveries are **at least once**: a crash after the receiver accepts a request but before Champagne records its acknowledgement can cause another POST. Persist `id` as a deduplication key before returning `2xx`. ### Concurrent deliveries and configuration changes [#concurrent-deliveries-and-configuration-changes] Each fiber fetches one job through pg-boss, which atomically claims due work across processes. No database connection or business-row lock is held while waiting for the receiver. The HTTP timeout is five seconds, with a ten-second overall delivery bound; the lease is not renewed. Each delivery uses its endpoint's URL and signing secret read when it is claimed. A secret rotation can finish while an earlier request is in flight; that request still uses the previous secret. Receivers should allow a short overlap during rotation for in-flight requests. Subsequent claims use the updated configuration. Pausing or deleting the endpoint, or deselecting an event, makes subsequent attempts skip delivery. A request that already read the configuration may still reach the receiver. Settlement requires an active job with the same retry count and an unexpired lease. Queue settlement and outbox outcome updates share a transaction. A late response from an expired or replaced attempt is ignored. A crash or interruption leaves the job active until pg-boss recovers it after expiration. This includes crashes after receiver acceptance, so receivers must deduplicate by event ID. ## Request contract (v1) [#request-contract-v1] The raw request body is UTF-8 JSON: ```json { "id": "a4e3d651-a06d-4e29-bd88-9870d6374a36", "type": "reservation.status_changed", "occurredAt": "2026-09-24T12:00:00.000Z", "clubId": "51000000-0000-4000-8000-000000000051", "data": { "reservationId": "05cb1d67-3935-4d87-a636-7ae1447f7060", "personId": "d60e28ed-8e79-4b9c-bc96-d1d93484af16", "categoryId": "1a3c3841-4d29-4394-8026-94341ef6f894", "previousStatus": "booked", "status": "confirmed", "startsAt": "2026-10-01T10:00:00.000Z", "endsAt": "2026-10-01T11:00:00.000Z", "guests": 1, "amountUnit": "credit_hundredths", "creditAmount": 2500, "monetaryAmount": null, "currency": null, "paid": false, "refunded": false } } ``` `Content-Type` is `application/json`; `X-Champagne-Event-Id` repeats `id`. `X-Champagne-Signature` is `t=,v1=`, calculated over the exact bytes of `t + "." + rawBody` using the club's one-time signing secret from Settings. Verify the HMAC in constant time, reject timestamps more than five minutes from your clock, then parse the JSON; do not reserialize before checking the signature. Keep a database unique constraint on the event ID. Return `2xx` only after durable acceptance, and process notifications asynchronously on your side. ## Emitted events [#emitted-events] All events have `clubId` and stable object IDs; no names, email addresses, tokens, application answers or invoice details are included. Fetch further context through the authorized GraphQL API if needed. | Type | When | `data` fields | | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `application.status_changed` | A candidature enters draft, submitted, under review, accepted or rejected | `applicationId`, `personId` (nullable), `previousStatus` (nullable), `status`, `revision` | | `membership.granted`, `membership.updated`, `membership.renewed`, `membership.removed` | A staff or Stripe-driven membership change is recorded (no unchanged replays) | `membershipId`, `personId`, `planId` (nullable), `expirationDate` (nullable), `paidPeriodEnd` (nullable) | | `reservation.created` | A member or staff booking is persisted (including initially confirmed free bookings) | Reservation fields listed in the example; `previousStatus` is null | | `reservation.updated` | A booking's dates, resource names or guest count change without changing status | Reservation fields listed in the example | | `reservation.status_changed` | Booking moves to another status, including payment confirmation, cancellation/refund or completion | Reservation fields listed in the example | | `reservation.attendance_changed` | A reservation is marked present, late, absent or unmarked | `reservationId`, `personId` (nullable), `categoryId`, `previousAttendance`, `attendance` | | `invoice.issued` | A matching Stripe invoice first appears as open/overdue/paid, or leaves draft | `invoiceId`, `personId`, `status`, `amount`, `currency` | | `invoice.paid` | A projected invoice first becomes paid | `invoiceId`, `personId` | | `invoice.credited` | A new credit note appears on an already projected invoice | `invoiceId`, `personId`, `creditNoteId`, `amount`, `currency` | Reservation `status` values are `pending`, `booked`, `confirmed`, `completed`, `canceled`; attendance is independent (`unmarked`, `present`, `late`, `absent`). `startsAt` and `endsAt` are null for undated conversation requests. `paid` means a Stripe payment intent has been recorded, not that a payment request has been created. Invoice amounts are in the smallest currency unit. Reservation amounts follow the explicit units below. A first sync of existing Stripe invoices can emit `invoice.issued`/`invoice.paid`; credit notes present on the initial projection are not emitted. A ticket scan does not mark attendance. Application payment/signature answers do not change application status and do not produce status events. Same-state retries and unchanged membership writes do not emit events. ### Reservation amount units [#reservation-amount-units] Reservation lifecycle events (`reservation.created`, `reservation.updated`, and `reservation.status_changed`) publish a discriminated amount contract: | `amountUnit` | `creditAmount` | `monetaryAmount` | `currency` | | ------------------- | ------------------------------ | ---------------------------- | --------------------------- | | `credit_hundredths` | Integer hundredths of a credit | `null` | `null` | | `currency_minor` | `null` | Integer currency minor units | Currency code such as `eur` | A booking priced at 25 credits emits `creditAmount: 2500`, even if the club sells 2 credits per euro. No conversion using the current exchange rate is performed. Zero-price credit bookings emit `creditAmount: 0`. This is the reservation price; it does not include the separate refundable credit deposit. The retained direct Stripe reservation checkout path uses currency minor units. Reservations with a persisted Stripe account, checkout request/session, payment intent or refund identifier emit `currency_minor`, including before payment is confirmed. A Stripe purchase of credits does not mark individual credit bookings as monetary. `paid` and `refunded` indicate Stripe payment/refund records only; `false` does not mean a credit booking has an unpaid balance. The former reservation `amount` field is removed. Previously queued or archived events without `amountUnit` retain their original payload and have ambiguous units: consumers must not interpret them as money or credits automatically. Invoice payloads retain their existing monetary `amount` and `currency` fields. ## Member notification events [#member-notification-events] The following events supplement the original catalog. New catalog entries are available to club administrators; clubs with an existing saved selection must explicitly select the new types. Member inbox/email delivery does not depend on that selection or on a webhook URL. | Type | Trigger | Additional `data` | | --------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | `reservation.proposed` | Staff changes a pending reservation's proposal | `reservationId`, `personId`, `proposedStartsAt`, `proposedEndsAt`, `proposedFree` | | `reservation.message` | A professional posts a chat message | `reservationId`, `personId` (no message body) | | `chat.message` | A professional starts or answers a customer chat | `chatId`, `personId` (no subject or message body) | | `guest.joined` | Guest creates their account from an invitation | `invitationId`, `personId`, `recipientId` (member inviter, nullable for staff invitations) | | `reservation.invitation_accepted` | Guest accepts a reservation invitation (by name, account or as a member) | `reservationId`, `inviteeId`, `personId` (nullable), `recipientId` (member inviter, nullable for staff invitations) | | `sponsorship.joined` | Sponsored person receives membership | `applicationId` (nullable), `sponsorId`, `personId`, `recipientId`, `eventKey` | | `reservation.reminder` | Confirmed reservation starts within 24 hours | `reservationId`, `personId`, `scheduledFor`, `validUntil`, `eventKey` | | `waitlist.joined` | Waiting member is booked, automatically when a place opens | `reservationId`, `personId`, `sessionId`, `occurrenceId` (both null for a time slot) | | `membership.renewal_due` | Stripe paid subscription period ends within 7 days | `membershipId`, `personId`, `scheduledFor`, `validUntil`, `eventKey` | | `session.canceled` | Future occurrence canceled | `sessionId`, `occurrenceId`, `personId` for each affected recipient, `reservationId` for booked members | `reservation.proposed` also includes an `eventKey` identifying the reservation and proposal version, plus `validUntil` set to the proposed start time. Member email delivery uses these fields to skip obsolete proposals; outbound webhooks retain the original lifecycle event. Cancellation emits a recipient event per affected reservation/waitlist entry; an empty occurrence emits an event without a person. Recipient-level notification preferences do not suppress external webhooks. `eventKey` deduplicates scheduled/join milestones at production; delivery still requires receiver deduplication by envelope `id`. Existing webhook pause/no-backfill semantics remain. Sponsorship joining is deduplicated per sponsored person and sponsor, not per add-on or renewal. When a guest accepts a reservation invitation, the member inviter gets a "Your guest accepted" notification instead of a second `guest.joined` alert. Staff inviters have no member inbox and receive a direct email when their guest accepts a reservation or club guest invitation. There are still no events for credit balances, sanctions, Mews/Book4Time stays or treatments, restaurant/SevenRooms no-shows, Lightspeed house accounts, Salto access or push/SMS preferences. ## Operations [#operations] `webhook_outbox` retains one row per event and endpoint: the event, `endpointId`, attempt count, settlement timestamp and generic `lastError` (`delivery-failed`, `not-selected`, `disabled`, `endpoint-deleted` or `not-configured`, never a response body or secret). pg-boss owns scheduling, leases and terminal failure state; an unsettled outbox row alone does not prove a retry is pending. Inspect delivery metadata without selecting payloads or signing secrets: ```sql select o.id, o.type, o."clubId", o."endpointId", o.attempts, o."deliveredAt", o."lastError", j.state, j.retry_count, j.retry_limit, j.start_after, j.started_on from webhook_outbox o left join pgboss.job j on j.id = o.id and j.name = 'outbound-webhooks' where o."deliveredAt" is null order by j.start_after nulls last; ``` Check `webhook.queue` logs for queued, active, retry, failed and completed counts and oldest queued age; `webhook.delivery` records sanitized outcomes. Investigate terminal failures and missing job rows before assuming automatic retry will recover them. There is no internal drain route or documented automatic replay operation. Protect database access, Terraform state and receiver logs; the receiver must not trust unsigned webhook bodies or assume delivery implies a user notification. ### Delivery dashboard [#delivery-dashboard] The production bootstrap stack provisions the private **Champagne webhook delivery** dashboard from `infra/scaleway/bootstrap/dashboards/webhooks.json` using the existing managed Cockpit Loki source. Its 27 panels cover delivery outcomes and rates, retry attempts, duration percentiles, queue states and oldest queued age, snapshot freshness, worker failures/startup and event-type/log drilldowns. Review/apply the existing bootstrap stack to publish the dashboard, and deploy the core JSON logger contract before expecting data; a dashboard definition does not prove ingestion is live. Effect's `Logger.consoleJson` emits `message` values `webhook.delivery`, `webhook.queue` and `webhook.worker`, with fields nested under `annotations`. Queries read `annotations.module`, `annotations.outcome`, `annotations.eventType`, `annotations.attempt`, `annotations.durationMs`, the queue counts/age, and worker operation/failure kind. Queue snapshots describe the same shared database from each API instance: panels take the **MAX of the latest sample per instance**, never sum replicas or accumulate repeated snapshots. The latest-sample window is three minutes; the MAX is a conservative sampled view, not an atomic live database count. Check freshness alongside counts; missing or stale samples are not healthy zeros. Native log retention limits historical views. Local development continues using its existing worktree Waggle; no local Grafana is provisioned. The dashboard adds no alert rules, ingestion/retention billing changes or new compute. ## Incoming Stripe webhook diagnostics [#incoming-stripe-webhook-diagnostics] The `/api/webhooks/stripe` and `/api/webhooks/stripe/accounts` endpoints return a server-generated `X-Request-Id`. Unexpected processing failures remain HTTP 500 with `Webhook processing failed` and generate one correlated, sanitized Effect error log. Missing or invalid signatures remain HTTP 400 without an unexpected failure log. Raw event bodies, Stripe signatures and provider error messages are not logged. This concerns incoming Stripe callbacks, not the outgoing club webhook signature or event-ID contract described above. # Create an object Describe an object, review its drawing and add it to your floor plan. Source: https://docs.champagne.dev-ukab.fr/en/ai-flows/artwork-generation Create custom artwork for furniture, fixtures and other features of your space. Start with a clear object name, then refine the preview until it suits your plan. ## 1. Describe the object [#1-describe-the-object] In the floor-plan editor, open the **Cosmetic** tab in the **Object library** and select the **Create object** plus button. Enter an **Object name**. Use a short description of one object. Useful examples include: * Lounger * Round table * L-shaped sofa * Rectangular swimming pool with steps Champagne supplies the drawing instructions: a top-down view, flat colors and simple outlines. Include a feature in the name when it matters to the object.
Create object dialog with the Object name field, Expert mode and Generate object button.
Enter the object name, then choose Generate object.
## 2. Generate the preview [#2-generate-the-preview] Select **Generate object**. The preview area shows that work is in progress while Champagne draws the object and converts it into editable shapes. Keep the dialog open while it works. Use **Cancel generation** to stop the current request. A fresh generation can produce a different interpretation of the name.
Create object dialog showing the artwork loading area and Cancel generation control during an example request.
The preview area during an example generation request.
```mermaid flowchart TD accTitle: Create an object accDescr: Enter a name, generate a drawing, convert it to shapes, review it and create a library object. Name[Enter the object name] --> Generate[Choose Generate object] Generate --> Draw[Champagne draws the object] Draw --> Vector[Convert the drawing into shapes] Vector --> Review[Review the preview] Review --> Choice{Happy with the result?} Choice -->|Yes| Create[Choose Create] Choice -->|Adjust an edge or feature| Brush[Use Magic brush] Brush --> Review Choice -->|Try another drawing| Generate ``` ## 3. Check the drawing [#3-check-the-drawing] Look at the outline, proportions and important details. For a lounger, check its backrest and arms. For a pool, check the steps and the shape of its edge.
Sample lounger preview with Magic brush, undo, redo, Regenerate object and Create controls.
An example lounger in the preview. Create adds the accepted drawing to your library.
Choose **Regenerate object** for another drawing, or use [Magic brush](/en/ai-flows/magic-brush) for a targeted adjustment. **Undo edit** and **Redo edit** let you compare successful versions during the current editing session. Keep the object name unchanged while comparing versions. Changing the name starts a fresh preview history for the new subject. ## 4. Place it on the plan [#4-place-it-on-the-plan] Select **Create** to add the artwork to your library. Choose the object from the library and place it on the canvas, then adjust its size and rotation. Select **Save plan** when the layout is ready. Custom artwork created through this dialog is decorative. Use the **Reservables** tab to place items associated with reservation resources. ## Getting a clearer result [#getting-a-clearer-result] Choose a recognizable object name and include only the structural details you need. Large, simple shapes usually remain easier to read at floor-plan scale. If tiny details become cluttered, try a simpler name and regenerate. The [vectorizing guide](/en/ai-flows/vectorizing) explains how the drawing becomes smooth shapes and what **Auto-lines** changes. # How the AI works A closer look at the prompts, tools and review loops behind the floor-plan controls. Source: https://docs.champagne.dev-ukab.fr/en/ai-flows/how-the-ai-works Each AI feature combines your input with instructions supplied by Champagne. For brush editing and plan import, an **agent** works in turns: it chooses an action, receives the result and decides what to do next. This page adds a little technical detail to the [user guides](/en/ai-flows). The prompt examples are shortened summaries of the instructions used by the app. The tool examples use illustrative data. ## Prompts: the instructions behind a click [#prompts-the-instructions-behind-a-click] A **prompt** describes the task and its constraints. Champagne assembles it from three parts: * **App instructions:** the drawing style, permitted actions and review requirements. * **Your input:** the object name, brush guide or uploaded plan image. * **Current context:** the latest drawing, available resources and results from earlier steps. Images and labels are treated as reference data. The app supplies the instructions that control the workflow. ### Generate object [#generate-object] For an object named "Lounger", the instructions can be summarized as: > Draw one lounger in a strict top-down view. Preserve its essential structure. > Use flat light-blue surfaces and thin navy outlines. Keep solid surfaces opaque. > Use the reserved green background around the object and through genuine gaps. The image service returns a PNG picture. Champagne removes the green background, trims the margins and [vectorizes the drawing](/en/ai-flows/vectorizing) into SVG: a format that describes shapes with lines and curves. That conversion runs through image-processing software on the server. **Generate object** follows this fixed sequence. The brush and import features use tool-calling loops to make and inspect successive changes. ### Apply changes [#apply-changes] For a brush guide around a tabletop edge, the instructions can be summarized as: > Interpret the red guide as an adjustment to the current object. Move the marked > edge to the suggested outline. Preserve the unmarked details, colors and position. > Apply the change to the existing shapes, then inspect the rendered result. The brush agent receives the current SVG, an image of the original object and the red guide. When **Classify brush intent** is enabled, a separate model first chooses the operation: inward cut, outward fill, removal, broad reshape or a combined cut and fill. That decision selects more specific editing instructions. ### Start import [#start-import] The import instructions can be summarized as: > Find the drawing area and preserve its proportions. Rebuild the main structure, > inspect it, then add furniture and fixtures. Use the built-in shapes where they > fit. Compare the draft with close-up reference views. Record uncertain details > and submit the result for human review. The agent also receives the original image dimensions, existing object templates and reservation resources with their assignment status. This context helps it place objects and respect existing resource assignments. ## Tools: the actions an agent can request [#tools-the-actions-an-agent-can-request] A **tool call** is a structured request to Champagne, such as "remove this shape" or "show me the draft". Each tool accepts a defined set of fields. Champagne checks the request, carries out the action and supplies the result to the agent. ### Magic brush tools [#magic-brush-tools] | Tool | Purpose | | ------------ | -------------------------------------------------------------------------------------------------------------------------- | | `update_svg` | Change the current drawing using element IDs: set or remove attributes, add a shape, or remove an element and its children | | `finish` | Accept the current drawing after its rendered preview has been supplied for inspection | An **element ID** is a stable name for a part of the drawing, such as an arm or outline. It lets the agent identify exactly which part to change. ### Plan import tools [#plan-import-tools] | Tool | Purpose | | ------------------- | -------------------------------------------------------------------------------------------- | | `register_source` | Choose the drawing crop and map its image coordinates onto the plan | | `view_reference` | Inspect a close-up of the original image | | `scene_transaction` | Add, update, remove or reorder plan objects, and adjust individual chairs attached to tables | | `list_objects` | Read the current objects, scene version, resources and warnings | | `view_objects` | Render the draft for inspection, optionally overlaid on the reference | | `create_template` | Generate a library object from its name and an optional shape description | | `finish` | Submit the draft and review notes for your review | Image crops use pixels from the original upload. Object positions use coordinates on the plan canvas. Registration establishes the mapping between those two spaces. ## A brush edit, step by step [#a-brush-edit-step-by-step] Suppose you scribble over a lounger arm and choose **Apply changes**. The current SVG contains a group of shapes with the example ID `left-arm`. 1. Champagne prepares the instructions, current SVG, original image and brush guide. 2. The agent identifies the targeted feature and requests `update_svg`. 3. Champagne validates the entire batch, applies it and renders a PNG preview. 4. The next request supplies the updated SVG and PNG alongside the original references. 5. The agent inspects the result. It can request a correction or accept it with `finish`. 6. The accepted version appears in your preview, ready for you to review. Here are the arguments for the example `update_svg` call: ```json { "operations": [{ "op": "remove", "id": "left-arm" }] } ``` The result is an updated SVG and its rendered image. Removing the group also removes its child shapes. The surrounding geometry stays in place. If validation fails, the batch is rolled back. The next request includes the unchanged drawing and a description of the failed operation, so the agent can send a complete corrected batch. ```mermaid sequenceDiagram accTitle: A Magic brush tool call accDescr: The user supplies a guide. Champagne gives it to the agent, validates the requested edit and returns a preview for inspection before showing the accepted result. actor U as You participant C as Champagne participant A as Brush agent U->>C: Red guide and Apply changes C->>A: Instructions, current SVG and reference images A->>C: update_svg with removal operation C->>C: Validate the batch and render a preview alt Edit is valid C->>A: Updated SVG and rendered PNG A->>C: finish with empty arguments C-->>U: Updated preview else Edit is rejected C->>A: Unchanged SVG and validation error A->>C: Corrected update_svg batch end ``` The brush agent rebuilds its context around the latest valid drawing at each step. It has up to eight model calls, including repairs and review, with at most two successful corrective batches after the initial edit. ## How the import loop progresses [#how-the-import-loop-progresses] The import agent takes one tool action per turn. A typical sequence is: 1. **Register:** `register_source` returns the drawing crop and coordinate mapping. 2. **Build:** `scene_transaction` adds the main structure and returns object IDs and a new scene version. 3. **Inspect:** `view_objects` renders that structure over the reference. 4. **Refine:** `view_reference` reveals smaller details; further transactions add furniture and correct placement. The agent checks each table's chair count, seating on each side and empty spaces against the source. 5. **Review:** regional views and a final full-scene view support the last checks. 6. **Submit:** `finish` supplies review notes and makes **Use draft** available. For example, `view_objects` can request a full-scene comparison with these arguments: ```json { "region": null, "scale": 1, "labels": "none", "overlay": true } ``` Champagne renders the scene, adds a tool response with view information and remaining time/turns, then supplies the PNG in an image message. The agent can compare the actual pixels before choosing its next action. These calls and results accumulate in the import conversation. Edits include an `expected_version`: the scene version the agent used to prepare them. Champagne checks it before applying a batch. A successful batch advances the version and returns stable object IDs for later edits. For seating adjustments, a `move_chair` operation identifies the table and chair, then supplies a desired position relative to the table. Champagne finds a safe position along the table outline and returns the resulting chair positions for the agent to inspect. The import has a ten-minute budget and up to 32 tool turns. It requires an early structural inspection, at least two distinct regional inspections and a final full view of the latest scene. Your review completes the workflow: check the notes, choose **Use draft**, adjust the layout and select **Save plan**. # AI tools for your floor plan Create objects, adjust their shape and bring an existing plan into Champagne. Source: https://docs.champagne.dev-ukab.fr/en/ai-flows Use Champagne's AI tools to draw objects for your space and turn a plan image into an editable layout. You review the results, make adjustments and choose when to save. ## Open your floor plan [#open-your-floor-plan] Open **Reservations**, choose your club and reservation category, then open its floor-plan view. Choose **Edit floor plan** to work on the layout. Your account needs permission to edit the category, and **Floor plan** needs to be enabled in its settings.
Example pool plan with the object library on the left, the layout in the center and plan settings on the right.
The editor with an example pool layout. Choose objects from the library, arrange them on the plan and select Save plan.
## Choose what you want to do [#choose-what-you-want-to-do] * **[Create an object](/en/ai-flows/artwork-generation):** enter a name such as "Lounger" or "Round table", then review the drawing. * **[Adjust a drawing](/en/ai-flows/magic-brush):** draw a red guide over the preview to remove a feature, move an edge or reshape an object. * **[Import a plan](/en/ai-flows/plan-import):** upload an image and review the layout as Champagne rebuilds it with individual objects. ```mermaid flowchart TD accTitle: Choose an AI floor-plan tool accDescr: Create an object from a name or import a plan from an image. Review the result, arrange the layout and save the plan. Start[Open your floor plan] --> Choice{What would you like to add?} Choice -->|One object| Name[Enter an object name] Name --> Preview[Review the drawing] Preview --> Brush[Adjust with Magic brush] Brush --> Preview Preview --> Create[Create the library object] Choice -->|An existing layout| Upload[Upload a plan image] Upload --> Review[Review the imported draft] Review --> Use[Choose Use draft] Create --> Arrange[Arrange your plan] Use --> Arrange Arrange --> Save[Choose Save plan] ``` ## Review and save [#review-and-save] **Create** adds an accepted drawing to the object library. Select it from the library and place it on the canvas. **Use draft** brings an imported layout into the editor. **Save plan** saves your layout changes. Check the position, size and appearance of each object. For bookable items, also check the assigned reservation resource. Decorative objects help describe your space. ## Understand the drawing [#understand-the-drawing] Champagne converts generated artwork into shapes that stay crisp as you zoom and resize. Read [How vectorizing works](/en/ai-flows/vectorizing) for a visual walkthrough of that conversion, including the **Auto-lines** setting. For a look behind the controls, read [How the AI works](/en/ai-flows/how-the-ai-works). It explains the prompts, agent tools and preview checks with a small tool-call example. For optional controls and common problems, see [Settings and help](/en/ai-flows/prompts-and-debugging). The screenshots in this guide show the actual interface with example data and sample artwork. Your drawings and processing times will vary. # Adjust an object with Magic brush Draw a guide to change an edge or remove a feature, then compare the result. Source: https://docs.champagne.dev-ukab.fr/en/ai-flows/magic-brush Magic brush is available in the **Create object** preview. Use it to adjust the generated drawing before adding the object to your library. ## 1. Turn on Magic brush [#1-turn-on-magic-brush] Generate an object and select the wand button labeled **Magic brush** in the preview's upper-left corner. Draw directly over the preview with your pointer. Your guide appears in red. ## 2. Show the change you want [#2-show-the-change-you-want] Use one clear gesture for each adjustment: | Your goal | What to draw | | ------------------------------- | ------------------------------------------------------------------ | | Remove a feature | A compact back-and-forth scribble over the feature | | Move an edge inward | A simple line inside the existing edge | | Extend an edge outward | A simple line outside the existing edge | | Change several sides | A broad continuous line around the sides you want to reshape | | Cut one area and extend another | A guide with clear sections inside and outside the current outline | For example, scribble over a lounger arm to request its removal. For a wider tabletop, draw the new outer edge and join it back to the existing surface. The red line is an approximate guide. Champagne smooths small wobbles and tries to connect the edited section naturally to the surrounding drawing. A removal scribble selects the whole feature, including its detail lines. Removing a feature on a solid surface fills in the surface behind it.
Sample lounger with Magic brush selected, a red guide near the left arm, and Clear sketch and Apply changes buttons.
A red guide on the example lounger. Clear sketch lets you redraw the guide; Apply changes submits it.
## 3. Apply and review [#3-apply-and-review] Select **Apply changes**. Champagne reads the guide, edits the drawing and checks a rendered preview. It may make a correction during that check. The updated artwork appears when this process finishes. ```mermaid flowchart TD accTitle: Magic brush workflow accDescr: Draw a red guide, apply the change, wait for an edit and preview check, then review or undo the result. Guide[Draw a red guide] --> Apply[Choose Apply changes] Apply --> Read[Champagne interprets the gesture] Read --> Edit[Adjust the shapes] Edit --> Check[Check the preview] Check --> Result[Show the updated artwork] Result --> Review{Keep this version?} Review -->|Yes| Create[Continue or choose Create] Review -->|Try again| Undo[Choose Undo edit] Undo --> Guide ``` Compare the updated outline with the change you intended. Check neighboring parts, gaps and surface colors as well. Use **Undo edit** to return to the previous successful version and **Redo edit** to restore it. ## Useful controls [#useful-controls] * **Clear sketch:** remove the red guide and draw another one. * **Cancel editing:** stop the current request and keep your current preview. * **Undo edit / Redo edit:** move between successful artwork versions. * **Create:** accept the current drawing into your library. Keyboard users can focus the editing area after enabling Magic brush. Press **Space** to begin a sketch, use the **arrow keys** to draw, and press **Space** again to finish. **Escape** leaves Magic brush. ## When the result needs another pass [#when-the-result-needs-another-pass] Apply one small change at a time. Use a compact scribble for removal and a simple continuous line for an edge. Keeping each guide focused makes the intended change easier to interpret. An unsuccessful edit leaves the current preview available. Retry with a clearer guide or choose **Regenerate object** to start from a fresh drawing. The optional **Classify brush intent** control is explained in [Settings and help](/en/ai-flows/prompts-and-debugging). # Import an existing plan Upload a plan image, review the reconstructed layout and bring it into the editor. Source: https://docs.champagne.dev-ukab.fr/en/ai-flows/plan-import Use **Import plan** to start from a photograph, scan or screenshot of an existing layout. Champagne rebuilds the furniture, fixtures and essential room separators as an editable draft. ## 1. Choose a clear image [#1-choose-a-clear-image] Use a PNG, JPEG or WebP file up to 6 MB. A straight-on image with readable lines gives the clearest starting point. Crop away surrounding phone controls and leave the complete drawing visible where possible. Select **Import plan** in the floor-plan editor. Drop your image into the dialog or use the file picker, then select **Start import**.
Import plan dialog showing the image file picker and Start import button.
Choose the plan image, then start the import. The reconstructed layout will appear in this dialog.
## 2. Watch the draft take shape [#2-watch-the-draft-take-shape] The preview updates as Champagne works. The status explains the current step: | Status | What Champagne is doing | | ----------------------- | ----------------------------------------------------- | | Reading plan | Opening the image and preparing the draft | | Registering source | Finding the drawing area and keeping its proportions | | Inspecting reference | Looking more closely at parts of your image | | Editing geometry | Adding or adjusting walls, furniture and other shapes | | Inspecting plan | Comparing the reconstructed layout with the image | | Creating library object | Generating an object using its name and shape | | Ready to review | Presenting the draft and its review notes | An import can take several minutes. Keep the dialog open while it runs. Select **Cancel import** to stop it. ```mermaid flowchart TD accTitle: Import a floor plan accDescr: Upload an image, watch the draft develop, review its details, choose Use draft, adjust the layout and save. Upload[Choose a plan image] --> Start[Choose Start import] Start --> Structure[Rebuild the main structure] Structure --> Details[Add furniture and fixtures] Details --> Compare[Compare the draft with the image] Compare --> Review[Ready to review] Review --> Inspect[Read notes and check the layout] Inspect --> Use[Choose Use draft] Use --> Adjust[Adjust objects in the editor] Adjust --> Save[Choose Save plan] ``` ## 3. Review the layout [#3-review-the-layout] At **Ready to review**, read the notes and compare the draft with your original image. Check these areas before accepting it: * **Main structure:** room proportions, wall boundaries and openings. * **Furniture:** number, position, size and orientation of each item. * **Seating:** chair counts on each side of a table, including ends that should stay clear. * **Fine details:** curves, windows, doors, stairs and partially visible areas. * **Spacing:** usable gaps between furniture and nearby structures. * **Bookings:** the reservation resource associated with each bookable object. The review notes describe uncertain areas and details that need attention. Small marks, complex stairs and partially cropped objects may need manual adjustment. Visible dining tables and standalone seats are proposed as reservable objects. Check these proposals before saving. Architecture and scenery stay decorative. ## 4. Use and save the draft [#4-use-and-save-the-draft] Select **Use draft** to add the result as a new plan tab in the editor. Adjust individual objects, remove unnecessary pieces and check their resource assignments. Select **Save plan** when you are satisfied with the layout. Saving creates resources for reservable objects that have no existing resource assignment. The editor supports up to 200 objects across the plan. If the imported draft would exceed that limit, make room in the existing plan before accepting it. A reservation resource already placed in the plan also needs its existing placement resolved. ## How an image becomes a layout [#how-an-image-becomes-a-layout] Champagne uses the source image as a visual reference. It uses the built-in artwork for doors and matching features such as plants, parasols and pools. Distinctive objects are generated from an object name and shape description, then become reusable library artwork through the [vectorizing process](/en/ai-flows/vectorizing). Tables keep individually editable chairs. Walls are included only where essential to explain separate rooms; outdoor and single-room layouts usually omit walls. Champagne compares the draft with the image in several passes, including an overlap audit. Stacking follows visible contours in the reference, not object category, booking status or creation order. Unclear overlap relationships are reported for review. The draft stays available during the import session. Finish reviewing it while the dialog is open. If the session expires or the service restarts, upload the image again. # Settings and help Choose optional drawing settings and recover from common generation, editing and import problems. Source: https://docs.champagne.dev-ukab.fr/en/ai-flows/prompts-and-debugging The default settings are a starting point for creating objects. Open **Expert mode** in the **Create object** dialog when you want to change how the artwork is generated or how brush guides are interpreted. ## Drawing settings [#drawing-settings]
Expanded Expert mode with Generation model, Auto-lines, Brush model, Thinking effort, Classify brush intent and Show more information controls.
Open Expert mode before generating to choose settings and enable additional progress information.
| Setting | What it does | | ------------------------- | ------------------------------------------------------------------------------------ | | **Generation model** | Chooses the service that draws the initial object: GPT Image 2.5 Sunburst or Flare | | **Auto-lines** | Controls how the drawing's dark outlines are converted into shapes | | **Brush model** | Chooses the assistant that interprets your guide and adjusts the drawing | | **Thinking effort** | Sets how much reasoning the brush assistant can use; higher settings may take longer | | **Classify brush intent** | Adds a separate gesture-reading step before the brush assistant edits the drawing | | **Show more information** | Shows extra progress and timing information for the request | The initial UI settings use **Sunburst**, **Auto-lines** enabled, **Gemini 3.8 Flash**, **Low** thinking effort and **Classify brush intent** disabled. With classification disabled, the brush assistant reads the guide and edits in one workflow. Enabling classification adds a first assistant that chooses the type of change. See [How vectorizing works](/en/ai-flows/vectorizing#what-auto-lines-changes) for the effect of Auto-lines on the finished drawing. ## What happens to your instructions? [#what-happens-to-your-instructions] Your object name tells Champagne what to draw. The app adds instructions for the top-down view, colors, outlines and empty background. For Magic brush, your red guide tells it where and how to change the current object. During an import, Champagne repeatedly looks at the image, places shapes, checks the result and corrects the draft. You see these activities through the progress labels and the updating preview. Names, artwork and plan images are sent to the configured AI services for processing. Use images that your organization is comfortable sharing with those services. ## While a request is running [#while-a-request-is-running] Generation and editing can take several minutes. Keep the preview open while the request runs. **Cancel generation**, **Cancel editing** and **Cancel import** stop their respective requests. If you want more detail, enable **Show more information** before starting the request. Progress may include drawing, vectorizing, editing and inspecting the result. ## Common problems [#common-problems] ### The drawing looks different from what I expected [#the-drawing-looks-different-from-what-i-expected] Use a more specific object name, then select **Regenerate object**. Describe the main shape and any essential feature. Use Magic brush when you want to keep the current drawing and change one part. ### The brush changed the wrong part [#the-brush-changed-the-wrong-part] Choose **Undo edit**. Draw a smaller, clearer guide and apply one change at a time. Use a compact back-and-forth scribble for removal and a smooth line for a new edge. ### The lines or small details look rough [#the-lines-or-small-details-look-rough] Try another generation with **Auto-lines** enabled. A simpler subject with fewer tiny details may give a clearer result. Review the object at its expected size on the plan as well as in the enlarged preview. ### A request failed or timed out [#a-request-failed-or-timed-out] Use the error notification's **Retry** action when offered. For a generation, you can also choose **Generate object** or **Regenerate object** again. If the message mentions a spending limit or an unavailable service, ask your administrator to check the AI service configuration. ### An imported draft needs corrections [#an-imported-draft-needs-corrections] Read the review notes and compare walls, openings, furniture and spacing with the source image. Choose **Use draft** and make the adjustments in the editor. For a fresh attempt, upload a clearer or more tightly cropped image. ### I want to leave without saving the layout [#i-want-to-leave-without-saving-the-layout] Use **Discard changes** in the floor-plan editor. A library object you already accepted with **Create** remains available in the library. Save the layout with **Save plan** when you are ready to keep the placements. # How vectorizing works How Champagne turns a generated picture into clean shapes and outlines for your plan. Source: https://docs.champagne.dev-ukab.fr/en/ai-flows/vectorizing **Vectorizing** turns a picture into shapes described by lines and curves. Those shapes stay crisp when you zoom in, resize an object or place it on a large plan. They also give Magic brush individual parts to adjust. Imagine tracing a tabletop onto transparent paper: follow its edge, fill the surface and draw its detail lines. Champagne performs a similar process automatically. ## From picture to shapes [#from-picture-to-shapes] ```mermaid flowchart TD accTitle: How vectorizing works accDescr: Start with a generated picture, remove the background, separate surfaces and dark outlines, trace their shapes, then compare the result and display the preview. Picture[Generated picture] --> Clear[Remove the temporary background] Clear --> Crop[Trim empty margins and preserve gaps] Crop --> Separate[Separate colored surfaces and dark outlines] Separate --> Surfaces[Trace the edges of each surface] Separate --> Lines[Follow the outlines and detail lines] Surfaces --> Combine[Combine shapes into the drawing] Lines --> Combine Combine --> Check[Compare the traced drawing with the source] Check --> Preview[Show the finished preview] ``` ### 1. Clear the background [#1-clear-the-background] The drawing service starts with a picture on a temporary green background. Champagne removes that background, leaving transparent space around the object and through genuine openings. It also trims empty margins. This lets the object sit naturally on the floor-plan canvas. Solid surfaces, such as a tabletop or seat cushion, remain filled. ### 2. Find surfaces and outlines [#2-find-surfaces-and-outlines] Champagne separates the drawing into colored areas and dark lines. Nearby shades are grouped into a small set of plan colors. Small stray color patches are cleaned up, and the system repairs tiny transparent defects inside solid surfaces. For a lounger, the seat and backrest become filled areas. The outer edge, arms and dividing lines provide the structure of the drawing. ### 3. Trace smooth shapes [#3-trace-smooth-shapes] The edges of each filled area are followed and converted into straight segments and curves. Shared boundaries help adjacent areas meet cleanly. The outline pass follows the middle of the original dark lines and fits a cleaner line through them. This step aims to preserve the footprint and recognizable details while smoothing small pixel-level irregularities. You may notice that very small details become simpler in the preview. ### 4. Check the result [#4-check-the-result] Champagne renders the traced drawing and compares it with the source. It checks whether important filled areas, outlines and details have survived the conversion. If the first tracing loses too much detail, it tries a more precise pass. Your preview shows the accepted drawing. Check its appearance at the size you plan to use, especially narrow gaps, small features and joined edges.
Example lounger preview showing filled surfaces, an outer contour and internal dividing lines.
The example lounger combines filled surfaces with outlines and internal detail lines.
## What Auto-lines changes [#what-auto-lines-changes] Open **Expert mode** in **Create object** to find **Auto-lines**. It starts enabled. * **Enabled:** Champagne follows the center of dark marks and rebuilds them as consistent-width lines with rounded joins. This gives furniture and fixtures a cleaner floor-plan appearance. * **Disabled:** Champagne traces the dark marks as filled shapes, preserving more of their original thickness and variation. Choose the setting before generating. Changing it while a preview is open applies to the next generation. Generate another version to compare the appearance. ## Results you may see [#results-you-may-see] **A cleaner, simpler drawing.** Small color variations and rough edges can merge into smoother surfaces and lines. Clear silhouettes and larger details usually convert more reliably. **A generation that needs another attempt.** If conversion cannot preserve enough detail within the drawing limits, the request can fail. Try a simpler object name or change Auto-lines and generate again. **A custom imported object with image detail.** Some complex reference crops are kept as an image inside the object when conversion reaches its detail or size limit. You can position, rotate and resize that object. Fine details can look softer as you zoom, and detailed brush editing may require a freshly generated vector version. For a whole imported plan, Champagne builds most of the layout from its built-in shapes. Vectorizing is used when it creates custom artwork for an unusual detail. # API tokens Tokens that let automation administer one club, or manage clubs on the platform. Source: https://docs.champagne.dev-ukab.fr/en/graphql/api-tokens An API token lets a tool such as the [Terraform provider](/en/terraform) call this API without a person signing in. A token belongs to one club and reaches only the areas of that club's configuration it was granted. Send it like a session token: ```http Authorization: Bearer chp_3f9c… ``` Secrets start with `chp_` followed by 48 hexadecimal characters. Only a hash is stored: the secret is returned once, by `createClubApiToken`, and cannot be read again. ## What a token can do [#what-a-token-can-do] A token is for configuration only. It reaches the operations below, which are the ones the [Terraform provider](/en/terraform) calls, and is refused everything else with `API tokens cannot use this operation.`: people, memberships, applications, reservations, billing, payment accounts, chats, the team, the platform, API tokens themselves, and public operations such as `plans` or `signOut`. An operation added to the API stays closed to tokens until it is added to this list. Each operation belongs to one area. A token is granted `read` or `write` on each area it needs; an area left out is closed to it, and the operation answers `This API token lacks the permission for this operation.` with `UNAUTHENTICATED`. | Area (`ClubApiTokenArea`) | Queries (`read` or `write`) | Mutations (`write`) | | ------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `settings` | `clubConfiguration` | `updateClubConfiguration` | | `plans` | `plan` | `createPlan`, `updatePlan`, `splitPlan`, `deletePlan` | | `reservation_catalog` | `reservationCategory`, `reservationResource`, `clubStaff`, `reservationSessionDefinitions` | `createReservationCategory`, `updateReservationCategory`, `deleteReservationCategory`, `setReservationCategoryLinks`, `createReservationResource`, `updateReservationResource`, `deleteReservationResource`, `createReservationSession`, `deleteReservationSession` | | `floor_plans` | `reservationFloorPlan` | `saveReservationFloorPlan` | | `housekeeping` | `housekeeping` | `setResourceChecklist` | | `person_fields` | `personAttributes`, `personTags` | `createPersonAttribute`, `updatePersonAttribute`, `deletePersonAttribute`, `createPersonTag`, `updatePersonTag`, `deletePersonTag` | | `webhooks` | `clubWebhookEndpoints` | `createClubWebhookEndpoint`, `updateClubWebhookEndpoint`, `deleteClubWebhookEndpoint`, `revealClubWebhookSecret` | | `newsletter` | `newsletterSettings` | `configureNewsletterSender` | `clubApiTokenIdentity` and `platformApiTokenIdentity` need no permission. On the operations of its areas a token passes the checks a club administrator passes for its club, including modules the platform turned off for the club (refused to administrators too): | Rule | Behavior | | -------------------------- | ---------------------------------------------------------------------- | | `read` on an area | The queries of the area. Its mutations are refused. | | `write` on an area | The queries and mutations of the area. | | Area not granted | Every operation of the area is refused. | | Other clubs | Refused, like for an administrator of another club. | | Blocked token | Resolves to no identity until it is unblocked: requests are anonymous. | | Expired or deleted token | Resolves to no identity, for good. | | Suspended or archived club | Same as a blocked token until the club is active again. | A token created with `hidesPersonalData: true` reads personal data pseudonymized, exactly like a staff account with the [`hidesPersonalData` restriction](/en/graphql/auth#account-restrictions): guest names in `housekeeping` and `reservationFloorPlan` read `Member` followed by a code, or `Guest`. Staff names are not member data and stay readable. Permissions, `hidesPersonalData`, the name and the expiration date are set when the token is created and cannot be changed afterwards: create another token, then delete the first. Activity recorded by a token names it `API token `. ## Identify the token [#identify-the-token] ```graphql query TokenIdentity { clubApiTokenIdentity { id name permissions { area access } hidesPersonalData clubId } } ``` Returns the token authenticating the request, or `null` for any other caller. Automation uses `clubId` as the `clubId` argument of every other operation. ## Permissions [#permissions] `clubApiTokens`, `createClubApiToken`, `setClubApiTokenBlocked` and `deleteClubApiToken` require a signed-in administrator of the club. Staff accounts are refused whatever their permissions, and so are API tokens. ## List tokens [#list-tokens] ```graphql query ClubApiTokens($clubId: ID! = "51000000-0000-4000-8000-000000000051") { clubApiTokens(clubId: $clubId) { id name permissions { area access } hidesPersonalData hint createdBy createdAt lastUsedAt expiresAt blockedAt status } } ``` Tokens are returned newest first, blocked and expired ones included. `hint` holds the last four characters of the secret, `createdBy` the name of the administrator who created the token (null once that account is deleted). `lastUsedAt` is refreshed at most once a minute. `status` is `blocked` while `blockedAt` is set, otherwise `expired` once `expiresAt` is reached, otherwise `active`. ## Create a token [#create-a-token] ```graphql mutation CreateClubApiToken($input: CreateClubApiTokenInput!) { createClubApiToken(input: $input) { secret token { id name status } } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "name": "Terraform, staging", "permissions": [ { "area": "plans", "access": "write" }, { "area": "settings", "access": "read" } ], "hidesPersonalData": true, "expiresAt": "2027-06-30T23:59:59+02:00" } } ``` | Argument | Rule | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Trimmed, 1–80 characters on a single line. | | `permissions` | At least one, each area listed once: otherwise `Give the API token at least one permission.` or `List each API token area only once.` | | `hidesPersonalData` | Optional, `false` by default. `true` pseudonymizes the personal data the token reads. | | `expiresAt` | Optional. An ISO 8601 date and time with an offset (`Use a valid expiration date.`), in the future (`The expiration date must be in the future.`). Left out, the token never expires. | A club holds at most 20 tokens, blocked and expired ones included; beyond that the mutation answers `Delete an API token before creating another one.` ## Block or unblock a token [#block-or-unblock-a-token] ```graphql mutation SetClubApiTokenBlocked($clubId: ID!, $id: ID!, $blocked: Boolean!) { setClubApiTokenBlocked(clubId: $clubId, id: $id, blocked: $blocked) { id status blockedAt } } ``` Blocking is immediate and reversible. Blocking an already blocked token keeps its first `blockedAt`. Unblocking never revives an expired token. The mutation returns `null` when the club has no token with this id. ## Delete a token [#delete-a-token] ```graphql mutation DeleteClubApiToken($clubId: ID!, $id: ID!) { deleteClubApiToken(clubId: $clubId, id: $id) } ``` Deletion is immediate and permanent: the token no longer authenticates and leaves the list. Returns `false` when the club has no token with this id. ## Platform tokens [#platform-tokens] A platform token lets automation manage the clubs themselves, as the [platform administration](/en/graphql/platform) does. It belongs to no club and reads no club data. Its secret starts with `chpp_` followed by 48 hexadecimal characters and is sent the same way. | Rule | Behavior | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Allowed operations | `platformSpaces`, `createPlatformClub`, `updatePlatformClub`, `setClubStatus`, `setClubFeatures`, and both identity queries. | | Everything else | Refused with `API tokens cannot use this operation.`: admins, owners, spaces, history, tokens and all club data. | | `read` access | `platformSpaces` only. | | Blocked, expired or deleted token | Resolves to no identity. | `platformApiTokenIdentity { id name access }` returns the platform token authenticating the request, or `null`; `clubApiTokenIdentity` is `null` for a platform token and conversely. Platform admins manage these tokens; club admins, space owners and API tokens are refused. ```graphql query PlatformApiTokens { platformApiTokens { id name access hint createdBy createdAt lastUsedAt expiresAt blockedAt status } } mutation CreatePlatformApiToken($name: String!, $access: ClubApiTokenAccess!, $expiresAt: String) { createPlatformApiToken(name: $name, access: $access, expiresAt: $expiresAt) { secret token { id name } } } mutation SetPlatformApiTokenBlocked($id: ID!, $blocked: Boolean!) { setPlatformApiTokenBlocked(id: $id, blocked: $blocked) { id status blockedAt } } mutation DeletePlatformApiToken($id: ID!) { deletePlatformApiToken(id: $id) } ``` Names, expiration dates, blocking and deletion follow the rules of club tokens; a platform token keeps a single `read` or `write` access instead of per-area permissions. The platform holds at most 20 tokens, and creations, blockings, unblockings and deletions are recorded in the [platform history](/en/graphql/platform#platform-history). # Application operations Start, resume, advance, list, inspect, and review club applications. Source: https://docs.champagne.dev-ukab.fr/en/graphql/applications ## Member account readiness [#member-account-readiness] `Application.memberAccountReady: Boolean` reports whether the linked member already has credentials in this club. It is available through the existing authorized application reads and mutation results; no password or credential material is returned. Null means readiness is unavailable and must not be treated as permission to create a password. After acceptance and completion of every after-approval step (including `afterApprovalCompletedAt` when steps exist), show password setup only when this field is false. When true, direct the applicant to their club's account sign-in or password recovery. The password mutation still validates completion and rejects replacing an existing credential. ## Reading applications [#reading-applications] `applications(clubId, page, pageSize, search, statuses, from, to, sort, direction)` returns a page of matching summaries for that club. The default order is creation date descending and then ID descending. Filters and sorting run before pagination. * `clubId: ID!` is required and validated as a UUID. * `page: Int! = 1` is one-based and accepts values from 1 through 1,000,000. Requests beyond the final page are clamped to the final available page. * `pageSize: Int! = 10` accepts 10, 25, or 50. * `sort: ApplicationSortField = created` accepts `created`, `applicant` (first name, then last name), `submitted` (submission timestamp), or `status`. * `direction: SortDirection = desc` accepts `asc` or `desc`. Each order ends with ID as a stable tie-breaker. Nullable names and submission timestamps follow database null ordering. Applicant names and statuses compare without case or accents (including decomposed Unicode); equivalent names use ID as the final tie-breaker. * `search: String` is an optional case- and accent-insensitive, literal substring of the combined first name, last name and email. Surrounding whitespace is ignored; at most 200 characters and no NUL are allowed. * `statuses: [ApplicationStatus!]` optionally selects one or more of `draft`, `submitted`, `accepted` or `rejected`. `submitted` also includes records left in the retired `under_review` state; including `under_review` is rejected. * `from: String` and `to: String` are optional inclusive submission dates in `YYYY-MM-DD` format, interpreted in UTC. Drafts without a submission date are excluded when either date is supplied. Invalid dates or a start date after the end date return `BAD_USER_INPUT`. * Selected statuses combine with OR; the status filter combines with the other filters using AND. An empty status list, omitted or null filters do not restrict results. ```graphql query { applications( clubId: "51000000-0000-4000-8000-000000000051" page: 1 pageSize: 10 statuses: [submitted] search: "Camille" from: "2026-09-01" to: "2026-09-30" ) { total page pageSize items { id firstName lastName email status submittedAt createdAt revision } } } ``` The page returns `items`, `total`, `page`, and `pageSize`. Each item is an `ApplicationSummary` (identity, club and onboarding references, status, progress, revision and timestamps). `afterApprovalCurrentStep` and `afterApprovalTotalSteps` give the after-approval progress, so the back office marks an accepted applicant who still owes a payment or a signature ("Steps 1/2") apart from one who has finished. `application(id, clubId)` returns the full snapshot for authorized staff, the linked member, or a caller supplying its scoped `X-Application-Token`; `clubId` is optional. Draft and submitted applications contain no person or account reference. An accepted application has a nullable `personId` populated by the acceptance transaction. Knowing its ID alone does not grant access. `bookings` lists the reservations made through its [booking step](#booking-a-visit) that are not canceled, earliest first, each with `blockId`, `reservationId`, `categoryId`, `categoryName`, `resourceName`, `startsAt`, `endsAt`, `guests`, `status` and the club `timezone`. It reflects later staff changes to the reservation. It is always a list on `application`; request it on one application at a time. ## Starting and advancing [#starting-and-advancing] `startApplication(onboardingId)` always creates a new draft from that exact onboarding. The new application's snapshot keeps the onboarding's configured block order, including the position of the required email verification block. An existing dossier or member account for the verified email is detected when that block is completed, again on submission and on acceptance. The onboarding's club and configuration and previously created applications are unchanged. The application stores the onboarding ID and revision plus immutable copies of its application and after-approval blocks. `Application.onboardingId` and `onboardingRevision` (also on `ApplicationSummary`) name the onboarding and the revision that snapshot was taken from; later edits to the onboarding never change them. Save the returned ID in the application URL to resume that draft. Saved answers are returned as `ApplicationAnswer { blockId value plan completedAt }`. `blockId` is the `id` of the snapshot block it answers, so clients match answers to blocks by ID rather than by position; `value` is the display value, `plan` the plan snapshot of a plan-selection block, and `completedAt` the time the answer was saved. `advanceApplication(input)` completes the current block when `stepIndex` is omitted: the current question while the application is a `draft`, the current after-approval step once it is `accepted`. Address, phone verification, identity verification and booking blocks can sit on either side and complete the same way. Supply the application ID plus the latest `revision`, then the fields required by that block: * `value` for short answer, long answer, and options blocks. * `firstName` and `lastName` for the required person-name block. * `address` (`AddressInput`) for the optional applicant-address block: `line1`, `line2`, `city`, `postalCode`, `state`, and `country`. `line1`, `city`, `postalCode`, and `country` are required together; an empty address is allowed only when the block itself is optional. * `planId` for plan selection. It must be one of the plan IDs captured in that onboarding block; legacy snapshots with an empty list allow any currently available club plan. * For email verification, first call `requestApplicationEmailOtp(applicationId, revision, email)`. It stores a salted challenge, advances the optimistic revision, and sends a random six-digit code that expires after ten minutes. Then pass the returned revision plus the same `email` and received `otp` to `advanceApplication`. Its `error` tells a malformed code ("Enter the six-digit code sent to your email."), an expired or replaced challenge ("This code has expired. Send a new code.") and a wrong code ("This code is incorrect. Check the latest email or send a new code.") apart. A challenge allows five incorrect attempts; after that every code, including the right one, returns "Too many incorrect codes. Send a new code." A new code can be requested one minute after the previous one ("Please wait one minute before requesting another code."); it resets the attempt count. `Application.emailOtpSentTo` and `emailOtpExpiresAt` expose the pending challenge's address and expiry, so the consumer reopens code entry for that address after a reload until the code expires. Verifying the email clears both. * For the phone-verification block, call `requestApplicationPhoneOtp(applicationId, revision, phone)` while it is the current block (otherwise "Phone verification is not the current step."), then pass the returned revision plus the same `phone` and received `otp` to `advanceApplication`. The number must include its country code; a number without `+` or `00` is read as French. Numbers are validated against the full libphonenumber metadata, and only numbers that can be mobiles receive a code (landlines return "Enter a mobile number: landlines cannot receive texts."). `Application.phone` stores E.164 (`+33612345678`); the block answer and the accepted person's phone number use the spaced international form (`+33 6 12 34 56 78`) that the back office saves. The code is sent by text message and follows the email rules: ten-minute lifetime, one minute between codes, five incorrect attempts. Errors name the phone instead of the email ("Enter a valid phone number with its country code.", "Send a code to this phone number first.", "Enter the six-digit code sent to your phone.", "This code is incorrect. Check the latest text message or send a new code."). A text that cannot be sent returns the updated application with "The code could not be sent. Please try again.". Because texts cost money and the mutation is public, only numbers from the allowed countries receive codes (the EU and EEA, Switzerland, the United Kingdom, Monaco and the French overseas departments unless the deployment configures others); other numbers return "Codes cannot be texted to this country.". By default each number receives at most five codes per hour across all clubs ("Too many codes were sent to this number. Try again in an hour."), each connection (client IP address) requests at most ten per hour ("Too many codes were requested from this connection. Try again in an hour."), each club sends at most one hundred per hour ("Phone verification is busy. Try again later.") and the platform sends at most five hundred per hour ("Phone verification is busy right now. Try again later."). The deployment can change each default, and a platform admin can give a club its own hourly limit with `setClubPhoneCodeCap`. Windows slide: a code counts for the hour after it was sent. Only an HMAC of the IP address, keyed by a server secret, is stored, and it is cleared once the code is older than an hour. Requests for the same number, connection or club wait for each other, so those caps hold under concurrent requests; the platform cap is a ceiling that concurrent requests for other numbers may pass by a few codes. When the public `phoneVerificationCaptchaSiteKey` query returns a key, render Cloudflare Turnstile with it and pass the widget's token as `captchaToken`. Each token works once. A missing, failed or expired token returns "Complete the security check before requesting a code." and no text is sent. A null key means the check is off. `Application.phoneOtpSentTo` and `phoneOtpExpiresAt` expose the pending challenge; `Application.phone` holds the verified number. Pass the completed block's `stepIndex` to both mutations to replace a verified number in a draft. Required answers, option membership, onboarding plan inclusion, plan existence, email format, OTP, block order, status and optimistic revision are validated before progress is saved. The final block saves its answer and leaves the status `draft`, with `currentStep` equal to the question count and `submittedAt: null`. The consumer then shows a review of all answers. A refresh resumes this review, and `stepIndex` edits remain available. To send the reviewed dossier, call `advanceApplication(input: { applicationId, revision, submit: true })` without `stepIndex`. All blocks must have saved answers. This explicit action changes `draft` to `submitted`, sets `submittedAt`, increments the revision and records exactly one applicant transition. Incomplete dossiers, stale revisions and repeat submissions are rejected without changing answers or creating another transition. For a draft, an optional zero-based `stepIndex` can instead edit an already completed name, text, options, or plan-selection block. The name becomes read-only once an identity check before approval has passed; identity and booking answers are never editable. The same answer validation and revision checks apply. The answer is replaced in place, with a new `completedAt`; later answers, verified email, current progress, and submission state are preserved. Negative or future indexes are rejected. Completed payment and signature blocks are read-only; previous answers also become read-only once a signature request exists or a signature or payment block has completed. Submitted and accepted applications cannot use this editing mode. Merely navigating backwards in the consumer performs no mutation. To replace a verified email in a draft, pass the completed email block’s `stepIndex` to `requestApplicationEmailOtp`, then to `advanceApplication` with the new email and code. Sending a code leaves the previous verified address and answer intact. Only a valid unexpired code replaces them and clears the challenge. Invalid codes, stale revisions, submitted applications and signature-locked drafts cannot change the email. Consumer Back/Continue revisit the email step, displaying the verified address and an Edit email action only on that step. Other questions do not repeat the email panel. The normalized first and last names are stored directly on the application and displayed as **Applicant** in the back office. The verified email is also stored directly on the application for acceptance, as is the verified phone number when the onboarding contains a phone-verification block. When the onboarding contains an applicant-address block, its normalized `Address` object is stored on the application and its formatted text (`line1, line2, postal code + city, state, country`) is kept as the block's answer for back-office display. Address and phone answered before approval are copied to the person on acceptance; answered as after-approval steps, they are also saved on the accepted person's record at once. ## Identity verification [#identity-verification] An `IDENTITY_VERIFICATION` block, before or after approval, is completed through [Stripe Identity](https://docs.stripe.com/identity) document checks. Sessions are created on the platform Stripe account, never on a club's connected account, so the step works for clubs without a merchant account and with billing turned off. `advanceApplication` refuses the step with "Verify your identity with Stripe before continuing." `startApplicationIdentityVerification(applicationId: ID!, revision: Int!)` returns `ApplicationIdentityVerificationStart { application verificationUrl error }`. It requires the applicant's access token (or staff access), an application whose current block is the identity step (the current question of a draft, or the current after-approval step of an accepted application), and its latest revision; otherwise it returns "Identity verification is not the current step." The server creates a `document` VerificationSession with `require_matching_selfie` set from the step, the applicant's email as `provided_details.email`, and a `return_url` of `/applications/?identity=returned` on the application's site (the club's hosted subdomain when the onboarding is published there). The create request carries an idempotency key per application, step and attempt, so concurrent clicks share one session. An open session is reused: Stripe's fresh single-use `url` is returned instead of creating another session. Open `verificationUrl` immediately; it is never stored and expires after 48 hours. `error` explains refusals, for example "Your identity document shows you are under the club's minimum age. Contact the club." or "Stripe is still checking your document. This page updates when it is done." The consumer uses Stripe's hosted redirect instead of the `stripe.verifyIdentity` modal: it needs no publishable key or Stripe.js, works the same on every hosted subdomain and on phones, and returns to the same application page as Checkout and Yousign. `refreshApplicationIdentityVerification(applicationId: ID!)` re-reads the open session from Stripe and applies its outcome. The consumer calls it on return from Stripe and every three seconds while the status is `processing`. It returns `ApplicationResult`. `Application.identityVerification` describes the latest attempt, or null before the first one: | Field | Description | | --------------- | ------------------------------------------------------------------------------------------------- | | `status` | `pending`, `processing`, `requires_input`, `verified`, `under_age`, `name_mismatch` or `canceled` | | `reason` | English explanation of a refused attempt, translated by clients; null otherwise | | `errorCode` | Stripe `last_error.code`, or `date_of_birth_missing` | | `documentCheck` | `verified` or `unverified` from Stripe's verification report | | `selfieCheck` | Same, when the step asks for a selfie | | `dateOfBirth` | `YYYY-MM-DD`, when the step has a minimum age | | `minimumAgeMet` | Whether the date of birth reaches the step's minimum age | | `nameMatches` | Whether the document's name contains the applicant's name, when the step requires it | | `canRetry` | Whether the applicant may start another attempt | | `retryAllowed` | Whether staff allowed a new attempt after this one | | `updatedAt` | Last time Champagne applied a Stripe outcome | The step completes only when Stripe reports the session as `verified` and every configured check passes. A failed Stripe check (`requires_input`) keeps the same session, so the applicant tries again with Stripe's new link. A name mismatch or an unreadable date of birth ends that session and lets the applicant start a new one. A date of birth under the minimum age (`under_age`) blocks the applicant until staff act. ### Webhooks [#webhooks] Stripe sends `identity.verification_session.processing`, `.verified`, `.requires_input` and `.canceled` to `POST /api/webhooks/stripe/identity`, an endpoint for events on the platform account itself ("Your account" in Workbench). The route verifies the signature with `STRIPE_IDENTITY_WEBHOOK_SECRET` and refuses live and connected-account events. It never trusts the payload: it re-reads the session from Stripe with `verified_outputs` and `last_verification_report` expanded, then applies the decision under a lock on the application row. Applying the same session again changes nothing, so redeliveries, out-of-order events and the applicant's own refresh are all idempotent; the step advances only while it is still the current step, moving `currentStep` before approval or `afterApprovalCurrentStep` after. A failed delivery returns HTTP 500 and Stripe redelivers it. ### Stored data [#stored-data] Each attempt is a row of `application_identity_verification` holding only the Stripe session ID, Stripe's status, Champagne's result, Stripe's error code, the document and selfie check results, the date of birth (for the age check) and whether the name matched. Names, addresses, document numbers, document images and selfies are never stored by Champagne; they stay in Stripe, where they can be redacted from the Identity Dashboard. Rows are deleted with their application. ### Staff actions [#staff-actions] Staff with `applications_manage` see `identityVerification` on the application and can act while the identity step is current, before or after approval: * `allowApplicationIdentityRetry(clubId, id, revision)` lets the applicant start a new attempt, for example after `under_age` or a mismatched name. An open Stripe session is canceled. * `completeApplicationIdentityStep(clubId, id, revision)` completes the step without a passing check. Like other staff overrides, the back office warns before confirming. The step's answer records the staff member's name. Both return `ApplicationResult` and refuse stale revisions. ### Configuration [#configuration] Identity uses the platform `STRIPE_SECRET_KEY` and is enabled by `STRIPE_IDENTITY_WEBHOOK_SECRET` (`whsec_`). Stripe returns the date of birth only to a restricted key with the **Identity verification results** and **Recent detailed verification results** read permissions: set it as `STRIPE_IDENTITY_RESULTS_KEY` (`rk_test_`). Without it, a step with a minimum age ends with `date_of_birth_missing`. Startup only checks that the Identity API answers for the platform key and leaves the step unavailable otherwise; Identity must also be activated in the Stripe Dashboard before it offers the restricted key's Identity permissions. Local stacks forward the events through the Stripe CLI listener with the same signing secret. ## Review [#review] `setApplicationStatus(clubId, id, revision, status, rejectionNote)` accepts `accepted` or `rejected` only when allowed by the current state, as detailed in [Status transitions and history](#status-transitions-and-history). It uses optimistic revision checks. Accepting atomically inserts an individual person with the application's name, verified email, verified phone number (which takes precedence over a phone typed in a question saved to the same person field), collected address, and club, writes answers whose question saves to a person field or member attribute as typed values (see [Member attributes](/en/graphql/person-attributes#save-onboarding-answers)), and stores the new person ID on the application, attaching visits booked before approval to that person. Rejecting cancels the application's active visits. A duplicate person email in the same club returns a readable error and leaves the application unchanged. The same email may already exist in other clubs. An accepted linked application is terminal. Payment, verification, and signature results remain part of the immutable answer history. Acceptance and rejection send applicant emails. The acceptance email always links to the application: "Complete your remaining steps" when after-approval steps exist, otherwise "Choose your password", since the applicant still sets a password before opening the account. The link uses the configured custom URL with an `applicationId` query parameter, or the built-in application URL when no custom URL is configured. The built-in URL, like the Stripe and Yousign return URLs below, uses the application's origin: the club's hosted site (`https://.`, the base domain being `CONSUMER_ORIGIN` without its `app`, `consumer`, `staging`, or `www` label) when the club has a hosted slug and the application's onboarding is published there, otherwise `CONSUMER_ORIGIN`. Applicants therefore return to the site where they applied. `rejectionNote` is optional, limited to 2,000 characters, stored as the current note only while rejected, and preserved on the corresponding history event. It is exposed on the full application and history, not on list summaries. The note is sent to the applicant: the rejection email includes it as the reason, and the applicant's own view of the application (through its scoped token) shows it. After acceptance, `advanceApplication(input)` cannot bypass a Plan payment step. Payment uses two dedicated operations: * `createApplicationCheckout(applicationId, revision, requestId)` derives the linked person, club, and snapshotted selected plan from the accepted application and opens hosted Stripe Checkout. The attempt is bound to the application and returns to `/applications/` on the application's origin. * `completeApplicationPayment(applicationId, revision, sessionId)` verifies the exact stored application attempt and advances only after its subscription has been projected from the signed `invoice.paid` webhook. Pending or expired sessions leave the application unchanged and return a readable error. The webhook also advances the step on its own, so payment completes even when the applicant returns from Checkout in another browser; a later call for the same confirmed session returns the application without an error. Document signatures use two dedicated operations: * `prepareApplicationSignature(applicationId, revision)` creates or resumes the Yousign request for the current signature block. It uses the application's snapshotted PDF, verified name and email, club ID, application ID, and block ID. The result includes the updated application and a short-lived HTTPS `signatureUrl` and an `embedded` flag. Sandbox requests are embedded on a dedicated consumer route because Yousign trial accounts reject custom redirect URLs. Production requests redirect to Yousign and use its success, error, and decline return URLs. Only one preparation runs at a time per application: a concurrent call (a double click or a second tab) returns the error "The document signature is already being prepared. Try again in a moment." instead of creating a second Yousign request. * `completeApplicationSignature(applicationId)` retrieves the stored request and signer directly from Yousign. It advances only when the signer status is `signed`; callback query parameters are not trusted as proof. Declined, expired, unfinished, missing, stale, and unconfigured requests return readable errors without completing the block. Signer names are sent to Yousign with plain apostrophes and hyphens, since its API refuses the typographic ones that mobile keyboards insert. When Yousign still refuses a name, preparation returns "The signature service does not accept some characters in your name. Contact the club to complete your signature." A failed preparation deletes the draft request it created (or cancels it once activated), and the server log records the provider status and the rejected parameters. Preparing a request stores its provider IDs on the application, so retries resume the same active signing link rather than creating another request. The provider `external_id` includes the club, application, and block IDs, keeping requests traceable across clubs. Progress is exposed through `afterApprovalAnswers`, `afterApprovalCurrentStep`, and `afterApprovalCompletedAt`; the application remains `accepted`. Accepted applications with unfinished after-approval steps receive up to three automatic finalization reminders: 3 days after acceptance, then 4 and 7 days after the preceding successful delivery (approximately days 3, 7, and 14). Each reminder links to the same custom or built-in continuation URL as the acceptance email. The schedule stops as soon as the final step is completed. Failed deliveries retry after one hour without consuming one of the three reminders. On startup, the worker also schedules the first reminder for older accepted, unfinished applications that do not yet have one. Configure `YOUSIGN_API_KEY` as a secret environment variable. The optional `YOUSIGN_API_URL` is restricted to `https://api-sandbox.yousign.app/v3` (the default) or `https://api.yousign.app/v3`. `CONSUMER_ORIGIN` must be an HTTPS origin, except for local development `.localhost` origins, and is used to construct success, error, and decline redirects on the application's origin. Yousign's custom redirects are unavailable on trial accounts. Sandbox mode therefore omits `redirect_urls`, enables Yousign's sandbox iframe-domain bypass on the signing link, and relies on the documented `signature.done` browser event. That event still triggers a server-side status check; it is not accepted as proof by itself. Private application data requires a scoped application token or an authorized session. ## Booking a visit [#booking-a-visit] An applicant books the current [booking step](/en/graphql/onboarding#booking-step), either a question of a draft or an after-approval step of an accepted application, with these operations. Each takes `applicationId`, so the scoped `X-Application-Token`, staff with application access, or the linked member can call it. ```graphql query BookingStep($applicationId: ID!, $date: String!) { applicationBookingCategory(applicationId: $applicationId) { id name description durationMinutes maxGuests timezone } applicationReservationSlots(applicationId: $applicationId, date: $date, guests: 1) { date partyFits slots { startsAt endsAt available } } } mutation Book($applicationId: ID!, $revision: Int!, $startsAt: String!) { bookApplicationReservation( applicationId: $applicationId revision: $revision startsAt: $startsAt ) { application { revision bookings { reservationId startsAt endsAt } } error } } ``` * `applicationBookingCategory(applicationId)` returns the step's category with its booking length, the largest party one resource holds and the club time zone. It returns `null` when the current block is not a booking block, or, after approval, when no person is linked. * `applicationReservationSlots(applicationId, date, guests)` returns the same `ReservationSlotDay` as [`reservationSlots`](/en/graphql/reservations#booking-rules-and-time-slots) for the step's category, including categories hidden from members. `date` is `YYYY-MM-DD` in club time and `guests` 1–500; other values receive `BAD_USER_INPUT`. It returns `null` under the same conditions as the category. * `bookApplicationReservation(applicationId, revision, startsAt, guests = 1)` books a slot start for the linked person, or, before approval, under the applicant's name with no person (it needs a verified name and email first: `"A verified name and email are required before booking."`). The booking follows the category's member rules (opening hours, notice, booking window, fixed length, party size and resource availability) but does not need a membership, since the applicant has none until the steps are done. Staff overrides do not apply. A booking already made for the step is canceled in the same transaction, so this also moves the visit; when the new time is refused, the previous booking is kept and the refusal is returned in `error`, for example `"The club is closed at this time."` or `"Nothing is available at this time for this party."`. A price on the category returns `"Onboarding visits are free. Ask the club to remove this price."`. * `cancelApplicationReservation(applicationId, revision)` cancels the step's booking, or returns `error: "There is no booking to cancel."`. Both mutations require the current `revision`, bump it on success and return the application. Outside the booking step they return `error: "Booking a visit is not the current step."`, and a stale revision returns `"This application changed. Reload it before continuing."`. Reservations are created with `source: member`, linked to the application and block, and confirmed at once, so the confirmation email and reminders go to the applicant (the application's email before approval) and staff see them in planning like other bookings. Accepting the application attaches its visits to the new person; rejecting it cancels its active visits. `advanceApplication({ applicationId, revision })` completes the step once a booking exists; its answer value is the booked start. A required step without a booking returns `error: "Book a visit to continue."`; an optional one completes with a `null` answer. After the step is done, the reservation can only be changed like any other reservation. ## Status transitions and history [#status-transitions-and-history] The server enforces these transitions: | Current status | Allowed next status | Entry point | | -------------- | ---------------------- | ------------------------------------------------------------- | | `draft` | `submitted` | Explicit confirmation with `advanceApplication(submit: true)` | | `submitted` | `accepted`, `rejected` | `setApplicationStatus` | | `under_review` | `accepted`, `rejected` | `setApplicationStatus` | | `rejected` | `accepted` | `setApplicationStatus` | | `accepted` | None | After-approval steps retain `accepted` | `under_review` is a retired state retained only for existing records and historical events. No operation can move a candidature into it; existing records can still be accepted or rejected. Review cannot move a record to `draft`, `submitted`, or `under_review`. An unchanged status is not a new transition and is rejected. Stale revisions, duplicate requests and concurrent decisions cannot append another event or send another decision email. After an uncertain response, reload the application before deciding whether another action is required. Accepting a rejected application clears the current rejection note while preserving it on the historical event. Acceptance remains final; automatic rejection of accepted, unfinished applications is not part of this increment. ```graphql query ApplicationHistory($clubId: ID!, $id: ID!) { applicationHistory(clubId: $clubId, id: $id) { id revision fromStatus toStatus occurredAt actorType actorId reason } } ``` Both arguments are required UUIDs. The result is `null` for an unknown application or a club mismatch, and an empty list for a known application without recorded transitions. Events are returned in ascending application revision order, without filters or pagination; revision is authoritative when timestamps coincide. There is no history update/delete API. History is restricted to staff with `applications_manage` in the application’s club (club administrators included; see [staff permissions](/en/graphql/auth#individual-staff-permissions)). | Field | Type | Meaning | | ------------ | ----------------------- | --------------------------------------------------------------------------- | | `id` | `ID!` | Unique event UUID. | | `revision` | `Int!` | Application revision resulting from the transition; unique per application. | | `fromStatus` | `ApplicationStatus` | Previous status; null only for recorded creation. | | `toStatus` | `ApplicationStatus!` | Resulting status. | | `occurredAt` | `String!` | Server timestamp with time zone, fixed when recorded. | | `actorType` | `ApplicationActorType!` | Origin: `applicant`, `staff`, or `system`. | | `actorId` | `String` | Authenticated staff ID for new review decisions; nullable for older events. | | `reason` | `String` | Trimmed rejection reason at decision time, otherwise null. | Creation records `null → draft`; final submission records `draft → submitted`. Applicant-origin actions are not proof of an authenticated identity. Review records `staff`, `actorId`, and `actorName` from the authenticated administrator. Clients cannot supply an actor or timestamp. The `system` value is reserved for server automation; no automated transition is introduced. Application changes, person creation on acceptance and the event insert commit in one transaction. A failed event insert rolls all of them back. History records decisions, not mail delivery; decision emails are sent after the transaction. Updating answers, sending an OTP, payment and signature completion do not rewrite decision history. Existing and seeded applications are not backfilled: their earlier transitions and actors are unknown. The first subsequent transition records its actual previous state and current server time, without fabricating dates from `updatedAt`. The back office explicitly reports incomplete history and displays event dates in the viewer's local time zone. ## Finding an application by verified email [#finding-an-application-by-verified-email] The consumer entry page links to `/applications/recover`. Recovery is scoped to one club: both mutations take a required `clubId`. Hosted onboarding sites use their own club; the shared consumer asks the applicant to choose a club (preselected from a `?club=` query parameter). Recovery does not create an application or change its status. Only the saved, verified `application.email` of that club's applications is matched, across statuses; an unfinished email verification is not a match. ```graphql mutation SendRecoveryCode($clubId: ID!, $email: String!) { requestApplicationRecovery(clubId: $clubId, email: $email) { error } } mutation FindApplications($clubId: ID!, $email: String!, $code: String!) { recoverApplications(clubId: $clubId, email: $email, code: $code) { error applications { id clubName status createdAt submittedAt } } } ``` Addresses are trimmed and lowercased. A syntactically valid address receives a six-digit code whether or not a matching dossier exists. The send response has no application identifiers, counts, or existence signal. Invalid addresses, unknown clubs and mail failures return an error. Sending is limited to once per minute per club and normalized address. Challenges persist per club and address as salted hashes, expire after ten minutes, allow five incorrect attempts, and are consumed atomically on success. A code issued for one club never verifies the same address at another club, and deleting a club removes its challenges. Resending after the cooldown replaces the previous code. Failed, expired, or already-used codes return an error and an empty applications array. Successful verification returns up to the 100 newest matching dossiers of the club (creation date descending, then ID descending), with the club name, status and creation/submission dates. No matching dossier is a successful empty array. There is no pagination or user-supplied sorting. Recovery results remain in memory in the consumer; refreshing the recovery page requires a new verification. Verified recovery returns a scoped `accessToken` with each application; send it as `X-Application-Token` for subsequent access. ### Existing email addresses [#existing-email-addresses] `advanceApplication` verifies the email code before checking for an existing dossier or person. Its result now includes an optional `continuation` with `applicationId`, `personId`, `message`, and `label`. Request it alongside `application` and `error`: ```graphql mutation VerifyApplicationEmail($input: AdvanceApplicationInput!) { advanceApplication(input: $input) { application { id revision currentStep } continuation { applicationId personId message label } error } } ``` An invalid code exposes no continuation. A valid code can return a continuation instead of advancing the new draft: resume a draft in the same club, view a submitted or rejected dossier, finish an accepted dossier, or open an existing person's account. A person in another club is asked to contact the club; no transfer or new membership is performed. A rejected dossier is retained and no automatic reapplication is permitted by this flow. When a continuation is returned, `application` and `error` are null and the existing records are not modified. The new draft is not assigned the duplicate email. Submission rechecks existing records, and staff acceptance refuses conflicting records. Transaction-scoped email locks serialize simultaneous verification and acceptance. Among existing dossiers for a club, non-drafts take priority, then the oldest creation date and ID. Anonymous drafts without a verified email can still coexist. Account access separately requires member or administrator authentication. ### Applicant photos [#applicant-photos] `Application.avatar` and `ApplicationSummary.avatar` return the current photo of the linked person, or `null` when there is no linked person or no photo. The photo is shared with People rather than copied into the candidature snapshot. Changing or removing it in People updates the linked application's photo on its next read. ## Sponsorship [#sponsorship] A club may add one required `SPONSORS` question with `sponsorCount` (1–5, default 2). The count is snapshotted with the application. Applicants answer with that many **sponsor codes**, one per line. Members generate these one-time codes from their account (see [sponsor codes](#sponsor-codes)) and hand them to the applicant. Codes are case-insensitive and may be typed with or without the dash or spaces (`abcd efgh`, `ABCD-EFGH` and `ABCDEFGH` are equivalent). Each answer must contain exactly `sponsorCount` distinct codes. When the step is saved, every code is checked and redeemed in the same transaction: * the code exists and belongs to the application's club; * it is unused, or already redeemed by this same application; * its author is still an active individual member of the club with a current membership (effective person status on today's UTC date, as elsewhere); * its author is not the applicant (same person or same email); * no two codes come from the same member. The first failing check is returned as the step error, a fixed sentence that names no code (for example `A sponsor code you entered has already been used.`) so clients can translate it. The stored answer value lists the sponsors' names, one per line; the codes themselves are not stored in the answer. Editing the step while the application is a draft releases the codes that are no longer part of the answer, so their authors can give them to someone else. Codes stay bound to an application once it is submitted, including after a rejection. `sponsors(applicationId: ID!, clubId: ID!): [SponsorPerson!]!` returns the members whose codes the application redeemed, each with `id`, `name` and `email`. Wrong club scope returns an empty list. It requires the `applications_manage` permission. When staff accept the application, its sponsors are copied onto the new person record. They then appear in [`personRelations`](/en/graphql/people#sponsors-and-invited-guests) and are notified (`sponsorship` category) when the person receives their first membership. Sponsorship never accepts an application by itself; the decision stays with staff. ```graphql query ApplicationSponsors($applicationId: ID!, $clubId: ID!) { sponsors(applicationId: $applicationId, clubId: $clubId) { id name email } } ``` ### Sponsor codes [#sponsor-codes] Signed-in members manage their codes with three operations. Each requires a member session for the given club; staff sessions are refused. * `mySponsorCodes(clubId: ID!): [SponsorCode!]!` lists the member's codes, newest first. Each has `id`, `code` (formatted `XXXX-XXXX`), `createdAt`, and nullable `usedAt` and `usedBy` (the name given on the redeeming application). * `createSponsorCode(clubId: ID!): SponsorCodeResult!` returns `{ code, error }`. Only active individual members with a current membership of the club can create codes, and at most 5 unused codes may exist at a time. Codes use 8 characters from an alphabet without ambiguous characters (no `0`, `O`, `1` or `I`) and do not expire. * `revokeSponsorCode(clubId: ID!, id: ID!): Boolean!` deletes an unused code and returns `false` when the code was already used or does not belong to the member. ```graphql mutation CreateSponsorCode($clubId: ID!) { createSponsorCode(clubId: $clubId) { code { id code createdAt } error } } ``` ## Text message history [#text-message-history] `applicationSmsHistory(id: ID!, clubId: ID!, page: Int! = 1)` has the same access rules, paging and null result as `applicationMailHistory` below. Each item is one phone verification code texted for the application: `id`, `phone` (spaced international form), `status`, `sentAt` and `statusAt` (time of the latest status). The code itself is never returned. Statuses mean: * `sending`: the text was reserved and is being handed to the SMS provider. * `sent`: the provider accepted the text; delivery to the phone is not yet confirmed. * `delivered`: the provider reported delivery to the phone. * `undelivered`: the provider reported a bounce or refusal. * `failed`: the provider or connection rejected the request; the applicant was told to retry. * `unknown`: a `sending` text older than one minute whose outcome was never saved. Delivery reports arrive from Brevo at `POST /api/webhooks/brevo/sms`, on a per-text URL signed with HMAC-SHA256. A report with a wrong signature returns 403. A delivered or undelivered text is final: later reports do not change it. Reports require a public HTTPS consumer origin, so local stacks keep texts at `sent`. ## Email history and reminder reliability [#email-history-and-reminder-reliability] `applicationMailHistory(id: ID!, clubId: ID!, page: Int! = 1)` is restricted to staff with `applications_manage` in the selected club (club administrators included). It returns `{ items, page, hasMore }` or null for an application outside that club. Pages contain at most 20 attempts, ordered by `startedAt` then `id`, newest first; `page` is bounded to 1–100,000. Concurrent sends can shift offset pages. Each item contains `id`, `kind` (`accepted`, `rejected`, `reminder`), `recipient`, `attempt`, `status`, `startedAt`, and nullable `completedAt`. The API never returns the message body, personal links, tokens or raw provider diagnostics. Statuses mean: * `sending`: a durable attempt was claimed; completion has not yet been recorded. * `accepted`: SMTP/API accepted the message for sending, not proof of inbox delivery. * `failed`: the provider or connection definitively rejected the attempt. * `unknown`: delivery may have occurred; automatic resending is blocked. An unfinished attempt older than one minute is displayed as `unknown`. Attempts use a separate database pool and transaction context so a business rollback cannot erase evidence that an email was sent. The same decision revision or reminder number cannot automatically resend after acceptance or uncertainty. Confirmed decision and reminder failures retain their attempt and retry after one hour. Uncertain reminders are rechecked but never automatically resent; investigate the provider before any manual recovery. Exactly-once inbox delivery is not guaranteed by SMTP or the current API. New acceptance and rejection decisions persist a pending notification in the same transaction as the decision. Sending starts after commit; the worker recovers pending notifications after restart. The delivery key retains the decision revision even if later application steps change the application revision. A newer decision replaces an older pending notification. Older applications without a pending notification are not re-notified. An uncertain decision attempt pauses automatic retries and remains visible in email history. A successfully accepted attempt is not resent if saving queue completion fails. A delayed acceptance email omits the continuation prompt when all steps are already completed. The mutation can return the saved application together with an email warning; the decision is already committed and should not be repeated to retry delivery. The existing schedule is unchanged: first reminder three days after acceptance, then four and seven days after the preceding successful send, at most three in total. The worker locks the application while checking and sending, serializing with other workers and finalization. Completed applications are not selected. A finalization that occurs after a send has already started cannot retract that email. No automatic rejection or sponsor reminder policy is introduced. Email history is available in the back-office application detail, refreshed every 15 seconds while visible. Earlier emails are not reconstructed. Only operational metadata is retained; a future application/person erasure workflow must also handle this journal, which intentionally has no cascading foreign key to the business transaction. # Authentication Member and staff sign in, passwords, person creation and account activation. Source: https://docs.champagne.dev-ukab.fr/en/graphql/auth Members sign in per club, staff sign in globally with access to several clubs. Send the returned token as `Authorization: Bearer ` on later requests. Automation authenticates with a club [API token](/en/graphql/api-tokens) instead of signing in. ## Member sign in [#member-sign-in] ```graphql mutation MemberLogin($clubId: ID!, $email: String!, $password: String!) { loginMember(clubId: $clubId, email: $email, password: $password) { token personId clubId } } ``` The same email can exist in different clubs as separate logins. The login email is the person's profile email, compared after trimming and lowercasing, so editing a member's email changes the address they sign in with. `memberViewer` returns the caller's `personId`, `clubId`, `guestExpiresAt` (null for regular members) and `external` (true for temporary accounts created by [external reservations](/en/graphql/external-reservations)), or `null` without a member token. Guests whose access has ended cannot sign in; see [guests](/en/graphql/guests). ## Choose a password after onboarding [#choose-a-password-after-onboarding] People created by an accepted application have no password yet. After the application is accepted and every after-approval step is complete, choose one: ```graphql mutation ChoosePassword($applicationId: ID!, $password: String!) { setMemberPassword(applicationId: $applicationId, password: $password) { token personId clubId } } ``` Only the applicant can call it: send the application's scoped `X-Application-Token`. Staff and member sessions receive `UNAUTHENTICATED`, even with `applications_manage` on the club. Passwords need 12-200 characters with at least one lowercase letter, one uppercase letter, one digit and one symbol. Seeded demo people use `password123`. Setting a password twice fails; duplicate emails in one club fail with a correction message. ## Create people from the back office [#create-people-from-the-back-office] `createPerson` requires a staff token with `members_manage` in the target club. Staff never choose the member's password: the person is created without a login and receives an activation email. ```graphql mutation AddPerson($input: CreatePersonInput!) { createPerson(input: $input) { id firstName lastName email } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "firstName": "New", "lastName": "Member", "email": "new@example.com" } } ``` `CreatePersonInput` takes `clubId`, `firstName`, `lastName` and `email`; there is no `password` field. Names must contain 1-100 characters after trimming and the email must be valid. Emails are unique per club after trimming and lowercasing; a duplicate fails with "This email is already used in this club.". Creation records `person.created`. After the person is saved, Champagne emails a single-use activation link built from `CONSUMER_ORIGIN`: `/activate-account?token=…&club=`. The link is valid for 7 days and the email is written in English and French. If `CONSUMER_ORIGIN` is not a bare `http(s)` origin, `createPerson` fails before creating anyone with "Activation emails are not configured. Please contact your administrator.". If delivery fails, the person is still created, the unsent token is discarded and the failure is logged; the person page then shows no pending link and staff can send one with `resendPersonActivation`. ## Activate a staff-created account [#activate-a-staff-created-account] ```graphql mutation Activate($token: String!, $password: String!) { activateMemberAccount(token: $token, password: $password) } ``` `activateMemberAccount` is public: the token is the credential. It applies the normal password policy, creates the member login with the person's current profile email, consumes every activation and reset link of that person, records `person.activated` (attributed to the system) and returns `true`. No session is returned; like a password reset, the member then signs in with `loginMember`. The consumer page at `/activate-account` links back to sign in for the club in the link. Failures are returned as `BAD_USER_INPUT` with these messages: | Situation | Message | | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Unknown, already used or superseded token, or a token sent to an email the person no longer has | This activation link is invalid or has already been used. Ask the club for a new one. | | Token older than 7 days | This activation link has expired. Ask the club for a new one. | | The person already has a password | This account is already activated. Sign in instead. | Reset tokens cannot activate an account and activation tokens cannot reset a password. `Person.activated` (in `people` and `peoplePage` items) and `PersonDetail.activated` are true once the person has a password. The back office shows an active person who is not activated as **Pending activation** instead of **Active**; suspended, booking-blocked, banned and disabled statuses take precedence. `status` itself and the `status` filter are unchanged, so pending people still match `status: active`. ## Resend an activation email [#resend-an-activation-email] ```graphql mutation Resend($personId: ID!, $clubId: ID!) { resendPersonActivation(personId: $personId, clubId: $clubId) } ``` Requires `members_manage` in the person's club. It issues a new 7-day link, emails it to the person's current profile email, then invalidates every earlier activation link and records `person.activation_resent`. When delivery fails, the new link is discarded, earlier links stay valid and the mutation fails with "The activation email could not be sent. Please try again.". It is refused with "This account is already activated. Sign in instead." when the person already has a password, with "This person has not finished their application steps yet." when the person comes from an accepted application whose after-approval steps are incomplete, and with "This person was not found." when the person does not belong to `clubId`. ## Staff sign in [#staff-sign-in] Staff have one global login and can access several clubs. Staff emails are stored trimmed and lowercased, so sign in is case-insensitive: ```graphql mutation StaffLogin($email: String!, $password: String!) { loginStaff(email: $email, password: $password) { token userId clubIds } } ``` `staffViewer` returns the caller's `userId`, `clubIds`, per-club `clubRoles`, assigned `reservationCategoryIds`, linked `resources { id clubId categoryId }`, and effective `clubRoles.permissions`, or `null` without a back-office token. Linked resources are the reservation resources the caller is linked to, limited to clubs they can access. The demo owner is `admin@example.com` with `password123` and access to every seeded club. Invalid credentials produce `BAD_USER_INPUT`; creating a person without staff access produces `UNAUTHENTICATED`. ## Sign out [#sign-out] ```graphql mutation SignOut { signOut } ``` `signOut` takes no arguments and ends the session named by the request's `Authorization: Bearer ` header, for members and staff alike. It is public and always returns `true`: a missing, malformed, expired, or already revoked token is not an error, so clients can call it unconditionally before discarding their stored token. Other sessions of the same account stay active. ## Authorization and sessions [#authorization-and-sessions] Tokens are opaque random credentials and only their SHA-256 hashes are stored. Member sessions expire after 30 days without use and staff sessions after 7 days without use; each use extends the session, at most once a day. Sign-out, password-reset revocation, administrative revocation, and account deletion end a session immediately. Each request checks the session and current club access. Missing and revoked tokens cannot access protected operations. Time-limited application-access tokens continue to expire independently. Guest member sessions stop resolving as soon as the guest's `guestExpiresAt` has passed. Members can read only their own `person` and their own checkout state, plus their club's `reservationCategories`. The member-only `createReservation` mutation derives identity from the session and requires an active account and current club membership. Admins manage the full back office within their clubs. Staff receive explicit per-club permissions; reservation operations additionally require an assigned reservation category. Members may list only their own upcoming reservations and start payment only for their own booked reservation; see [reservations](/en/graphql/reservations). Internal `PersonDetail.note` is always null for members. Club administrators can read and manage people, applications, sponsorships and billing only within their assigned clubs. `peoplePage` defaults to the administrator's first club when no club is specified; `people` includes only authorized clubs. Unknown operations are protected by default. Club directory and application configuration remain public so a new applicant can choose a club and apply. Staff directory results exclude inaccessible clubs. `staffViewer` additionally returns `name`, `email`, `isSpaceOwner`, `isPlatformAdmin`, `readOnly`, `hidesPersonalData` and `closedClubs`, and each `clubRoles` entry lists the club's `disabledFeatures` (see [Platform administration](/en/graphql/platform)). Owners see admin management, while owners and admins see staff management. Review transitions store the administrator ID and name from the authenticated session; clients cannot choose them. History displays the current name of the linked staff account. If the account is no longer available, its recorded name is retained. Email addresses are not used as display names. ## Individual staff permissions [#individual-staff-permissions] The source requirements are the action/actor inventories in `s3://champagne-storage-beb238f6/champagne-cadrage-bloc-1.md` §2.4 and `s3://champagne-storage-beb238f6/champagne-cadrage-bloc-2.md` §2.1–2.5. The latter explicitly lists role/permission management by Direction and Technique, but does not prescribe a fixed role matrix. Champagne therefore uses individual permission selections, independently for every club, instead of inferring access from job titles. The existing administrator role manages staff permissions; only the space owner manages administrators. A staff permission cannot promote its holder or delegate access. `staffSpace.permissionCatalog { id label group implies }` returns the assignable permissions in display order. `group` (`members`, `operations`, `finance` or `configuration`) groups them in the access editor, and `implies` lists the read permissions granted automatically with each one, exactly as authorization applies them; the editor shows those as "Included automatically". `staffSpace.members.clubPermissions` and `staffSpace.invitations.clubPermissions` return stored grants. `staffViewer.clubRoles.permissions` returns effective grants, including read dependencies and all permissions for club administrators. | Permission | Authorized actions | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `members_read` | Member profiles, internal notes, membership/status history, signed documents, attendance summaries and credit wallets | | `members_manage` | Create/edit profiles and photos, send member notifications, inspect/revoke member sessions, resend activation emails, invite guests | | `member_notes` | Edit internal profile notes | | `member_status` | Change member status, including suspension and banning | | `tags_manage` | Create, rename and delete member tags and set them on profiles | | `memberships_manage` | Assign/revoke memberships and initiate membership, add-on or credit checkout | | `applications_manage` | Read/review applications, transitions, email history and sponsors | | `billing_manage` | Read/refresh invoices and issue credit notes | | `planning_read` | Read reservations, availability, attendance, invitees and floor plans in assigned categories | | `reservations_manage` | Create/edit reservations, propose dates, refund deposits, cancel sessions and manage reservation invitees in assigned categories | | `attendance_manage` | Validate tickets, mark attendance and record early check-outs in assigned categories | | `availability_manage` | Edit their own default week, extra hours and time off | | `chats_manage` | Read/send reservation messages and manage the personal inbox in assigned categories | | `customer_chats` | Start, read and answer customer chats on any subject and manage the personal customer chat inbox | | `housekeeping_manage` | Read/update housekeeping, checklist checks and consumption in assigned categories | | `catalog_manage` | Configure club plans, categories, resources, sessions, blocks and floor plans | | `settings_manage` | Configure club settings, onboarding, credits, merchants, the newsletter sending domain and webhook integrations, including signing secrets | | `newsletters_manage` | Write, test and send member newsletters | The profile-write permissions (`members_manage`, `member_notes`, `member_status`, `tags_manage`, `memberships_manage`) and `customer_chats` include `members_read`. Operational planning permissions and `catalog_manage` include `planning_read`. These dependencies never grant another write permission. Selecting a member when booking requires `members_read` in addition to `reservations_manage`. Creating temporary guest access also requires `members_manage`. Catalog access exposes the club's category configuration and floor plans; access to individual reservations still requires an assigned category. `availability_manage` does not include `planning_read` and only edits the caller's own availability. ```graphql mutation StaffPermissions( $spaceId: ID! $userId: ID! $clubIds: [ID!]! $categories: [ID!]! $grants: [StaffClubPermissionsInput!]! ) { updateStaffAccess( spaceId: $spaceId userId: $userId clubIds: $clubIds reservationCategoryIds: $categories clubPermissions: $grants ) } ``` Example `$grants`: ```json [ { "clubId": "51000000-0000-4000-8000-000000000051", "permissions": ["members_read", "member_notes"] }, { "clubId": "51000000-0000-4000-8000-000000000052", "permissions": ["planning_read", "attendance_manage"] } ] ``` `inviteStaff` accepts the same `clubPermissions` argument. The selected permissions are persisted with the invitation and applied on acceptance. Acceptance rechecks the inviter's current club-management scope and the reservation-category assignments. Changes to an existing member's access invalidate pending invitations for that member. Both mutations replace access within the acting administrator's manageable clubs; other clubs are preserved. Grants for a club absent from `clubIds`, duplicate clubs, duplicate permissions, and unknown permission names are rejected. Omitted grants and explicit empty lists grant no operational permissions. Staff without reservation categories can still use their other granted modules. Removing all club access is allowed. The owner cannot be edited or removed, and an administrator cannot remove a staff account with access outside that administrator's clubs. Sessions load stored permissions on every API request: revocation applies to the next request without signing out. Back-office navigation and actions use the effective viewer permissions and periodically refresh them. The server independently protects operations, including direct GraphQL calls and forged category IDs. New operations are denied to staff until explicitly included in the permission policy. Personal account settings remain available with no operational permissions. ## Account settings and invitations [#account-settings-and-invitations] ```graphql mutation UpdateAccount($name: String!) { updateStaffAccount(name: $name) } mutation ChangePassword($current: String!, $next: String!) { changeStaffPassword(currentPassword: $current, password: $next) } mutation Invite($spaceId: ID!, $clubIds: [ID!]!, $typeIds: [ID!]!, $email: String!) { inviteStaff( spaceId: $spaceId clubIds: $clubIds reservationCategoryIds: $typeIds role: staff email: $email ) } mutation CancelInvite($spaceId: ID!, $email: String!) { cancelStaffInvitation(spaceId: $spaceId, email: $email) } mutation AcceptInvite($token: String!, $name: String!, $password: String!) { acceptStaffInvitation(token: $token, name: $name, password: $password) { token userId clubIds } } ``` Updating the name requires an authenticated staff session and a name of 1–100 characters, but no password. `changeStaffPassword` separately requires the current password and a valid new password, then invalidates all staff sessions. `inviteStaff` sends an invitation email and returns `true` only after successful delivery. No invitation token or copyable link is returned to the caller. The email names the inviter (their name, or their email when it is empty) and the invited clubs, and contains a single-use activation link, built from the configured `WEB_ORIGIN`, valid for 48 hours. Sending another invitation to the same email in the same space replaces the previous one. An email whose account already has access to one of the chosen clubs is refused with `This person already has access to one of these clubs. Edit their access instead.`: change that person's access instead. Delivery failure invalidates the new token and returns a retryable error. Acceptance rechecks the inviter's current management permissions, club scope, role, and reservation-category assignments. `cancelStaffInvitation` deletes the pending invitation of that email in the space, so its link stops working, and returns `true`. It requires the rights needed to send that invitation (the owner for an admin invitation, otherwise an administrator of every invited club) and records `staff.invitation_canceled`; an email without a pending invitation is refused with `This invitation is no longer pending.` `acceptStaffInvitation` atomically creates/links the account, consumes the invitation, and returns a 12-hour staff session (`token`, `userId`, `clubIds`). The frontend stores this session and opens the first authorized club directly. An existing administrator must enter their current password; their name and password are not overwritten. ## Password recovery [#password-recovery] ```graphql mutation Forgot($kind: String!, $email: String!, $clubId: ID) { requestPasswordReset(kind: $kind, email: $email, clubId: $clubId) } mutation Reset($token: String!, $password: String!) { resetPassword(token: $token, password: $password) } ``` Use `kind: "staff"` for the backoffice or `kind: "member"` plus the club ID for members. The request returns true without revealing whether an account exists. Known accounts receive a single-use 30-minute link via the configured mail provider. Repeated requests within a minute are suppressed. Links use `WEB_ORIGIN` for staff and `CONSUMER_ORIGIN` for members, never a caller-supplied redirect. A successful reset revokes existing sessions and other reset links. Password policy applies to every new password. A member request for a person in that club who has no password yet (created by staff and never activated) sends a new 7-day activation link instead of a reset link, with the same neutral `true` response. An accepted applicant whose after-approval steps (payment, signature…) are incomplete gets no link: they choose their password with `setMemberPassword` once the steps are done. Earlier activation links are invalidated once it is delivered. Requests within a minute of the previous activation link, including the one sent at creation, are suppressed. ## Member activity [#member-activity] ```graphql query Activity($personId: ID!, $clubId: ID!) { memberSessions(personId: $personId, clubId: $clubId) { lastLoginAt active activated activationExpiresAt } } mutation Revoke($personId: ID!, $clubId: ID!) { revokeMemberSessions(personId: $personId, clubId: $clubId) } ``` Both operations require `members_manage` in the person's club. `lastLoginAt` is an ISO timestamp or null if the person has not signed in. `active` means at least one session exists, not that the browser is currently online. `activated` is true once the person has a password. `activationExpiresAt` is the expiry of the latest activation link still stored for a person who is not activated (it may be in the past), or null when the person is activated or no link is pending. Revocation invalidates all member sessions immediately on the server; open clients recheck their viewer periodically. ## Applicant access and development accounts [#applicant-access-and-development-accounts] `startApplication` returns an `accessToken` scoped to the created application. Send it as `X-Application-Token` to read or advance that application before member login. It expires after seven days. Mailbox-verified `recoverApplications` results each include an `accessToken`. An OTP-verified continuation from `advanceApplication` also returns a token for its destination application. The consumer stores these per application in session storage; a fresh browser session uses mailbox recovery. An application UUID alone never grants access. Club administrators and the linked member can also access the application using their normal session. `demoMembers(clubId: ID!): [DemoMember!]!` returns `id`, `name`, and `email` for seeded accounts only when `DEMO_SEED=true`, including production builds of demo deployments. Otherwise it returns an empty list. Results are restricted to seeded demo person IDs with login credentials in the requested club; real members are never listed. Real applicants are not assigned the demo password by seeding. In demo mode, the consumer login prefills the first returned member's email and the existing seeded demo password, both on initial selection and when changing clubs. Switching clubs clears the previous credentials while the new club loads; clubs without demo accounts stay blank. Manual edits survive late responses and background refreshes. Selecting a demo account explicitly restores its demo credentials. Signing in still uses the normal authenticated login mutation; no session is created automatically and non-demo accounts are never prefilled. ## Account restrictions [#account-restrictions] Two independent switches restrict a back-office account, whatever its role and permissions. They are set with `inviteStaff(readOnly:, hidesPersonalData:)` and `updateStaffAccess(readOnly:, hidesPersonalData:)` (both default to `false`), returned by `staffSpace.members` and `staffSpace.invitations`, and reported to the account itself by `staffViewer { readOnly hidesPersonalData }`. They are stored per space and apply to the whole account: an account restricted in one of its spaces is restricted everywhere. The space owner is never restricted. Only the owner can change an admin's restrictions; admins change those of staff. **`readOnly`** — every query the role and permissions allow keeps working, and every mutation is refused with `UNAUTHENTICATED` and the message `This account is read only.` The exceptions are the account's own `updateStaffAccount`, `changeStaffPassword`, `updateStaffProfile` (own profile only) and `reportBug`, signing in and out, and platform operations for a platform admin. Staff document uploads over REST answer `403`. **`hidesPersonalData`** — the account reads the same records, pseudonymized: * A person's name becomes `Member` followed by a six-character code derived from the person's ID, so the same person carries the same pseudonym in lists, the planning, invoices and the activity history. On `Person`, `PersonDetail`, `Application` and `ApplicationSummary`, `firstName` is `Member` and `lastName` is the code. Names without a linked person (walk-in or external customers, invitees) read `Guest`. * Email, phone, address, birth date, gender, photo, company identifiers, custom attribute values, application answers, internal notes, status and rejection reasons, customer sheets, invoice payer details and invoice PDF links are returned empty or `null`. * Chat subjects and message bodies read `Hidden`. * Activity entries about a person, application, reservation, invoice or membership carry the pseudonym and no `note` or `changes`; configuration and team history is unchanged. * `peoplePage`, `people`, `applications` and `invoices` refuse the `search`, `minAge`, `maxAge`, `gender` and `attributes` arguments, and the following operations are refused, all with `This account cannot access personal data.`: creating or editing a profile, photo, note or attribute values, sending a member notification or newsletter test, downloading a signed document, inviting a guest or reservation invitee, creating a staff reservation, writing chat messages or customer sheets, and managing the team (which could otherwise grant an unrestricted account). Combining both gives a technical account that can inspect production without changing it or learning who the members are. Staff names and emails are not member data and stay visible. Restrictions are re-read on every request. ## Admin and staff management [#admin-and-staff-management] A space groups clubs under an owner. Clubs belong to one space and back-office accounts can belong to multiple spaces. Owners manage admins and staff; admins can manage staff within the clubs they administer. Staff cannot delegate access. New invitees enter their name when accepting an invitation. ```graphql query Spaces { staffSpaces { id name } } query Staff($spaceId: ID!) { staffSpace(spaceId: $spaceId) { id name ownerId clubs { id name } reservationCategories { id clubId name } members { id name email isOwner clubIds role reservationCategoryIds } } } mutation StaffAccess($spaceId: ID!, $userId: ID!, $clubIds: [ID!]!, $typeIds: [ID!]!) { updateStaffAccess( spaceId: $spaceId userId: $userId clubIds: $clubIds reservationCategoryIds: $typeIds ) } mutation RemoveStaff($spaceId: ID!, $userId: ID!) { removeStaffAccess(spaceId: $spaceId, userId: $userId) } ``` `staffSpaces` lists spaces the caller can manage, sorted by name. `staffSpace` returns the admins, staff, invitations, clubs, and reservation categories in the manageable scope. The owner sees the whole space. A club admin who does not own the space sees only the members and invitations with access to at least one club they administer, plus staff and invitations with no club in the space (the owner is not listed unless they hold such access); people who only work in the space's other clubs are left out. These lists have no filtering or pagination. Empty spaces return empty lists. Foreign spaces and unauthorized callers are rejected. Club IDs must be distinct, belong to the selected space, and number at most 200. Invitations require at least one club. Access edits accept an empty list to revoke every club in that space and preserve access to other spaces. The owner cannot have their own club access edited here. Edits invalidate pending invitations for that person in the same space. Existing sessions see current access on their next server request. Staff assignments must reference reservation categories from the selected clubs. Removing a staff member removes their access and planning assignments immediately. Removing a staff member, or withdrawing their access to a club, also unlinks them from that club's resources (`ReservationResource.staffUserIds`), so those resources stay editable and their slots no longer follow the former coach's availability. Existing bookings are kept. Only the owner can invite, edit, or remove admins; the owner account itself cannot be removed. ### Pending invitations [#pending-invitations] `staffSpace(spaceId: ID!)` also returns `invitations { email clubIds role reservationCategoryIds readOnly hidesPersonalData expired }`, ordered by email. These entries appear in the matching Admins or Staff table with an invitation-pending or invitation-expired status and a button that cancels the invitation with `cancelStaffInvitation`. No token or invitation URL is returned. ### Demo staff accounts [#demo-staff-accounts] With demo seeding enabled, all these accounts initially use `password123`: | Email | Name | Club access | Role | | ----------------------------------------------------------- | ------------------ | ------------------------- | ----------- | | [admin@example.com](mailto:admin@example.com) | Demo Administrator | Ukab Clubs, Vosges, Astel | Space owner | | [ukab.staff@example.com](mailto:ukab.staff@example.com) | Sport coach | Ukab Clubs | Staff | | [vosges.staff@example.com](mailto:vosges.staff@example.com) | Alex Bernard | Vosges | Admin | | [multi.staff@example.com](mailto:multi.staff@example.com) | Louise Moreau | Vosges, Astel | Admin | | [tech.staff@example.com](mailto:tech.staff@example.com) | Technical support | Ukab Clubs, Vosges, Astel | Admin | | [chloe.dubois@example.com](mailto:chloe.dubois@example.com) | Chloé Dubois | Ukab Clubs | Staff | | [ines.laurent@example.com](mailto:ines.laurent@example.com) | Inès Laurent | Ukab Clubs | Staff | | [manon.petit@example.com](mailto:manon.petit@example.com) | Manon Petit | Ukab Clubs | Staff | | [lea.garnier@example.com](mailto:lea.garnier@example.com) | Léa Garnier | Ukab Clubs | Staff | `tech.staff@example.com` is read only with personal data hidden (see [Account restrictions](#account-restrictions)). The four Ukab Clubs stylists manage only the "Soin en salon de beauté" category, whose resources carry their names. Additional staff accounts are created only when their email does not already exist. Reseeding preserves their edited names, passwords and club permissions, including removed access. Only owners and club administrators can manage staff or invitations. Newly seeded coaches receive planning, reservation, attendance, chat and housekeeping permissions; reseeding preserves edited permissions. Staff edits to a member's profile email also update their existing login email atomically. Password recovery uses the updated address; links issued to a different address are no longer valid. Existing sessions and passwords remain unchanged. See [Email consistency](/en/graphql/people#email-consistency). Camille is assigned to the seeded Ukab reservation categories and therefore lands in Planning. ## Staff activity history [#staff-activity-history] Every staff change is written to one append-only activity log, in the same database transaction as the change itself. The acting user comes from the authenticated session, never from mutation input. Reads are not recorded. Changes made through Stripe webhooks and background workers are attributed to the system; member self-service changes are attributed to the member. ```graphql query StaffActivity($spaceId: ID!, $userId: ID!, $page: Int!) { staffActivity(spaceId: $spaceId, userId: $userId, page: $page) { page hasMore items { id occurredAt action actorName clubId clubName subjectType subjectId subjectLabel personId personName note changes { field before after } } } } ``` `staffActivity(spaceId: ID!, userId: ID!, page: Int! = 1)` returns the actions performed by one member of the space, newest first, 20 per page, ordered by `occurredAt` and `id`. `page` must be between 1 and 10,000. New entries can shift offset pages; refresh page 1 to see recent actions. Access follows team management: the space owner can read every member's history, including team changes (invitations, access updates, removals and staff documents), which carry no `clubId`. Club admins can read the history of staff members only, limited to actions in the clubs they administer. Other callers receive `UNAUTHENTICATED`; a user outside the space or an admin viewed by a non-owner is rejected with the team-management error. `action` is a dotted verb such as `person.status_changed`, `reservation.attendance_marked`, `plan.updated` or `webhook.secret_revealed`. `subjectType` and `subjectId` identify the changed record, and `subjectLabel` is its name when the action happened (a person, plan, resource, webhook URL or email). `personId`/`personName` identify the affected member when there is one; `personName` and `actorName` reflect current names. `changes` lists modified fields as display strings (`before`/`after` are null when empty); secrets, tokens, passwords, file contents and message bodies are never stored. `note` holds the reason or note entered with the change. `staff.invited` and `staff.access_updated` describe access with the same fields: `role` (invitations only), `clubs`, `permissions` (per club, e.g. `Vosges: members_read, tags_manage`) and `reservationCategories`. An invitation lists only the fields it grants, so the history shows who gave which right whether it came from an invitation or a later edit. Every staff mutation must declare the activity it records, or be explicitly exempt, in `staffMutationActivity`; a unit test fails when a new staff mutation is missing. Exempt mutations only create checkout links, refresh provider data, mark chats as read, or change the caller's own account. ## Staff professional dossiers [#staff-professional-dossiers] The Team page opens an existing account's **Professional profile** and **Access** tabs. A dossier belongs to `(spaceId, userId)`, independently of dossiers in other spaces. It is available after invitation acceptance. No member-facing profile is published. The space owner can manage every dossier in that space, including their own. An administrator can manage a staff dossier only when the person has staff access to at least one club they administer, or when the person is still a member of the space without access to any club (so it can be reopened and given access again). A person with any administrator role in the space (or its owner) can only have their dossier managed by the owner. Ordinary staff, including the dossier subject, and members have no access. These rules are checked against current database access for every read, write and file request. ```graphql query { staffDocuments(spaceId: "SPACE_UUID", userId: "STAFF_UUID") { id kind title number issuer issuedOn expiresOn filename contentType size } } ``` `staffDocuments` returns all dossier documents ordered by kind, title and ID, without pagination or filters. `kind` is `professional_card`, `diploma` or `other`; multiple documents of each kind are supported. `title` is required (1–200 trimmed characters). `number` and `issuer` are nullable strings of at most 200 characters. `issuedOn` and `expiresOn` are nullable valid `YYYY-MM-DD` dates; expiration cannot precede issue. `size` is the byte count. Storage keys are never exposed. ```graphql mutation { updateStaffDocument( spaceId: "SPACE_UUID" userId: "STAFF_UUID" id: "DOCUMENT_UUID" input: { kind: professional_card title: "Professional card" number: "DEMO-123" issuer: "Issuing organization" issuedOn: "2026-01-01" expiresOn: "2031-01-01" } ) { id title expiresOn } } ``` This mutation replaces the metadata; omitted optional fields are cleared. It preserves the file. `removeStaffDocument(spaceId, userId, id)` removes metadata and queues private-file deletion durably; repeated deletion returns `true`. Cleanup runs immediately and retries every minute if storage is unavailable. Files use authenticated HTTP rather than GraphQL payloads: * `PUT /api/staff-documents/:spaceId/:userId/:id`: multipart `file` and `metadata` (JSON with all metadata fields, using null for absent optional fields). Use a client-generated document UUID for creation and reuse it when retrying. The same URL replaces an existing document's file and metadata atomically after storage succeeds. A failed upload leaves the previous document intact. * `GET /api/staff-documents/:spaceId/:userId/:id`: attachment download with `Cache-Control: private, no-store` and `X-Content-Type-Options: nosniff`. Both endpoints require the staff Bearer token. PDF, JPEG and PNG files must have matching detected signatures and MIME types, be nonempty, and be at most 10 MiB. Requests are bounded to that limit plus 64 KiB multipart overhead. Invalid inputs return 400, oversized bodies 413, unauthenticated requests 401, unauthorized scopes 403, missing documents 404, and unconfigured storage 503. Unexpected failures are masked. GraphQL uses `BAD_USER_INPUT` for invalid metadata and `FORBIDDEN` for dossier authorization failures after the staff-management gate. The UI shows **Not expired**, **Expired**, or **Expiry not provided** using the Europe/Paris calendar date; expiration is inclusive of its stated date. This is only a date indicator, not certification. No reminders, planning restrictions, or document version history are provided. Storage uses the existing private `TICKET_S3_*` configuration with the separate `staff-documents/` key prefix. With no S3 configuration in development, files live under `/app/.local/staff-documents` (override: `STAFF_DOCUMENT_STORAGE_DIRECTORY`). Partial S3 settings and production never fall back to local storage. Interrupted uploads are queued for cleanup after one hour. Removing team access makes the dossier inaccessible to ordinary administrators; the dossier remains associated with its space and account and is available again if that account rejoins. ### Expired documents in the team directory [#expired-documents-in-the-team-directory] `staffSpace(spaceId: ID!).members.expiredDocumentCount: Int` counts professional documents whose expiration date is strictly before today's calendar date in `Europe/Paris`. Documents expiring today remain current until the end of the day; missing expiration dates are not counted. The count is scoped to this team space. It returns `0` for an accessible dossier with no expired documents and `null` when the viewer cannot consult that person's dossier. In particular, other administrators' and the owner's counts are hidden from club administrators, as are staff dossiers without a shared managed club. Ordinary staff and members cannot query `staffSpace`. ```graphql query TeamDocumentStatus($spaceId: ID!) { staffSpace(spaceId: $spaceId) { members { id name expiredDocumentCount } } } ``` The team screen filters the returned members locally by a positive count; this adds no server filter, sort or pagination argument. Invitations have no dossier and are excluded from the expired-documents filter. Counts refresh after document changes. The document list keeps its categories and displays expired documents first within each category, then sorts by expiry date with undated files last. The private preview uses the authenticated file download endpoint and releases its temporary browser URL when closed. It grants no public or member access. ### Approaching document deadlines [#approaching-document-deadlines] Staff members also expose nullable `expiringDocumentCount`, using the same dossier permissions and team-space scope as `expiredDocumentCount`. It counts expiry dates from today through today plus 30 calendar days, inclusive, in Europe/Paris. Expired and undated documents are excluded. Unauthorized counts are null. The team directory offers an “Expires within 30 days” client-side filter; there are no automatic reminders or planning restrictions. ### Dossier consultation details [#dossier-consultation-details] `StaffMember.nextDocumentExpiry` returns the earliest expiry date on or after today (Europe/Paris), including dates beyond 30 days. It is null when there is no upcoming expiry or the viewer cannot access the dossier; the same space and role checks as the document counts apply. `StaffDocument.addedOn` is the first upload's date in Europe/Paris. It is assigned by the server and preserved by metadata edits and file replacements. Earlier documents without a recorded upload date return null. The dossier's search filters titles and issuing organizations locally, ignoring case and accents, while retaining category grouping and expiry ordering. Search does not change GraphQL arguments or pagination. ## Login attempt limits [#login-attempt-limits] Member and staff password logins consume a shared PostgreSQL quota before looking up credentials or verifying the password. Each normalized account has 10 attempts per 15-minute window. Member accounts are scoped to their club; staff accounts have a separate scope. Successful logins also count and do not reset the window. Each network peer has an additional quota of 100 attempts per minute across all accounts and both login operations. The peer address comes from the server's connection, never from `X-Real-IP` or GraphQL arguments. When that connection comes from a loopback address (`127.0.0.1`, `::1` or `::ffff:127.0.0.1`), which in production is the Caddy proxy running in the same container, the first valid IP address of `X-Forwarded-For` is used instead, so each client keeps its own quota. The header is ignored from any other peer and when it holds no valid address. Behind another proxy (such as the development Caddy container), its users share the peer quota. Requests without transport address information share an `unknown` bucket. Windows begin on the first accepted attempt and expire using the database clock. Concurrent requests and multiple API instances share atomic counters. Rejected attempts do not extend the expiry. Expired rows are reclaimed after one day during later login requests; account and peer identifiers are stored as SHA-256 digests. Quota exhaustion returns the same `Invalid email or password.` response as invalid credentials. It never triggers password verification or creates a session. Passwords over 200 characters are rejected before verification as well. ## Member-visible coach profiles [#member-visible-coach-profiles] `staffViewer` and `staffSpace.members` include nullable `bio` and `avatar` fields. Staff can edit their own profile; a space owner or an administrator can edit a staff member they manage in that space. Profile fields belong to the global staff account and are shared across its clubs. ```graphql mutation UpdateCoachProfile($bio: String, $avatar: String, $spaceId: ID, $userId: ID) { updateStaffProfile(bio: $bio, avatar: $avatar, spaceId: $spaceId, userId: $userId) } ``` Omit `userId` to edit yourself. Editing another user requires their `userId` and `spaceId`; the server checks team membership, administrative scope and shared club access. Only the space owner can edit another administrator. The mutation returns `true` on success. It replaces both profile fields: omitted, null or empty fields clear the existing value. Biographies are trimmed and limited to 4,000 characters. Photos use PNG or JPEG data URLs, with a verified file signature and a decoded size of at most 512,000 bytes, matching member profile photos. Members see only the coach's `id`, `name`, `bio` and `avatar` through messages in their own reservation conversations; see [reservations](/en/graphql/reservations). `StaffViewer.qualifications` and `StaffMember.qualifications` contain the diploma and professional-card metadata shown in the coach profile preview, scoped to the associated clubs. Original staff documents remain available only through the administrative dossier. # Sandbox membership billing Club-scoped direct charges, hosted Checkout and session-specific confirmation. Source: https://docs.champagne.dev-ukab.fr/en/graphql/billing Billing uses the club's selected [merchant profile](/en/graphql/merchants) and its connected Stripe account. Development, staging and production all use sandbox; live keys and events are refused. Checkout requires an authorized member, club administrator or scoped application access token, depending on the operation. Accepted Application/Onboarding payment steps reuse this same billing contract and require the selected club's merchant to be ready. A club with the `billing` module turned off refuses every Checkout and the payment portal with `BAD_USER_INPUT` ("Payments are turned off for this club."); see [Clubs with billing turned off](/en/graphql/platform#clubs-with-billing-turned-off). ## Offers [#offers] ```graphql query MembershipOffers($clubId: ID!) { membershipOffers(clubId: $clubId) { ready reason offers { planId name description amount currency interval } } } ``` `clubId` is a required local club UUID. No filters, sorting or pagination arguments exist. The non-null `MembershipOffers` response has `ready: Boolean!`, `reason: String` and `offers: [MembershipOffer!]!`. An unconfigured merchant, inactive payments or unprepared catalogue has explicit readiness feedback rather than a purchasable partial catalogue. Payout readiness is displayed separately in Finance and is not the Checkout payment gate. Ready offers retain non-null `planId: ID!`, `name: String!`, `description: String!`, `amount: Int!`, `currency: String!`, `interval: String!`. The seeded catalog starts with three annual euro plans, but club administrators can manage subscription plans and their monthly/yearly pricing in the back office. Add-on plans remain in the plan catalog but are not membership Checkout offers; they are sold through the add-on flow below. Products/Prices are resolved from club → merchant → local plan mapping, never from a universal `plan.stripeId`. ## Add-ons use credits [#add-ons-use-credits] List `plans(clubId)` and select type `addon`. These prices are in hundredths of a club credit. `payAddonCredits(clubId, planId)` derives the signed-in member, debits their balance atomically and grants the add-on. Retrying an already owned add-on returns success without another debit. Use [Club credits](/en/graphql/credits) for packs, exchange rates, top-up Checkout and balance/history queries. The legacy `createAddonCheckout` mutation rejects new cash purchases. Existing cash checkouts retain their status query and webhook fulfillment. Membership subscriptions continue using the cash billing APIs below. ## Create Checkout [#create-checkout] ```graphql mutation CreateMembershipCheckout($personId: ID!, $clubId: ID!, $planId: ID!, $requestId: ID!) { createMembershipCheckout( personId: $personId clubId: $clubId planId: $planId requestId: $requestId ) { url sessionId } } ``` All four arguments are required UUIDs. The person must exist and belong to the requested club. The plan must be one of the club's active subscription offers, the merchant and catalogue must be ready, and existing Stripe membership restrictions still apply, including expired Stripe-backed memberships. Invalid input or inconsistent associations reject the request. Stripe and unexpected failures surface as GraphQL errors, not a successful redirect. The return is `MembershipCheckout!`, containing `url: String!` (hosted HTTPS Stripe Checkout) and `sessionId: String!`. Keep `requestId` stable when retrying the same person, club and plan attempt after an uncertain response. An open session can be resumed; an expired attempt permits a new UUID. The browser prevents duplicate pending submissions. An expired retry returns `extensions.code: CHECKOUT_EXPIRED`; discard that attempt's cached UUID and retry with a new one. Transient errors retain the same UUID. Selecting a different plan expires the previous open session before reserving the new attempt. The server persists the attempt with person, historical club, merchant/account, plan, Price and session. Application attempts also persist the application ID, use `/applications/` success/cancel returns, and cannot be reused to complete another application. Customer identity is person + connected account. Calls use that connected account explicitly, with direct charges and no Champagne commission. The session and subscription carry the reconciliation metadata. Changing the browser's club does not change an already-created session's historical context. ## Confirm the returned session [#confirm-the-returned-session] Member purchases return to the consumer's `/account/membership?club=` tab (application payments return to `/applications/` on the club's hosted site when the application's onboarding is published there, so the applicant keeps their session). The success URL includes `checkout=success` and `checkout_session_id={CHECKOUT_SESSION_ID}`; add-on purchases also carry `kind=addon`. The browser sends that exact session back with its person and club: ```graphql query MembershipCheckoutStatus($personId: ID!, $clubId: ID!, $sessionId: String!) { membershipCheckoutStatus(personId: $personId, clubId: $clubId, sessionId: $sessionId) { status } } ``` The non-null `MembershipCheckoutStatus` contains `status: String!`: `pending`, `confirmed` or `expired`. The server verifies the stored attempt and its person/club, retrieves the session in its connected account, checks its subscription and waits for that subscription's paid projection. Another membership does not confirm this session. Invalid or mismatched session context is an error. There are no filters, sorting or pagination arguments. The consumer polls every two seconds while pending, stops at confirmed/expired, refreshes the account on confirmation, and offers retry on error. A missing session ID is explicit. The redirect never creates membership and pending/error states do not invite another payment. ## Application Checkout [#application-checkout] After acceptance creates the applicant's person, a current Plan payment step uses the plan snapshot already selected in the application: ```graphql mutation CreateApplicationCheckout($applicationId: ID!, $revision: Int!, $requestId: ID!) { createApplicationCheckout( applicationId: $applicationId revision: $revision requestId: $requestId ) { url sessionId } } ``` The application must be accepted, linked to its person, at a Plan payment step, and have a selected plan. `revision` is the current optimistic revision and `requestId` has the same retry semantics as account Checkout. The server derives person, club and plan from the application rather than trusting them from the browser. Its Stripe success return includes the exact Checkout session ID. ```graphql mutation CompleteApplicationPayment($applicationId: ID!, $revision: Int!, $sessionId: String!) { completeApplicationPayment( applicationId: $applicationId revision: $revision sessionId: $sessionId ) { application { id afterApprovalCurrentStep revision } error } } ``` Completion verifies that the session belongs to this application and waits for the same paid membership projection used by `membershipCheckoutStatus`. A browser redirect, completed Checkout, unrelated membership, wrong application, or missing webhook cannot advance the step. Pending and expired sessions return readable `error` values with the unchanged application; confirmed payment adds an auditable after-approval answer and advances exactly once. The signed `invoice.paid` webhook advances the Plan payment step itself, in the same transaction that projects the paid membership, when the subscription comes from an application Checkout. The applicant therefore does not need to return to the browser session that started Checkout. When the step was already advanced this way, `completeApplicationPayment` returns the current application with a null `error` for that confirmed session, so a late browser return completes normally. Starting another Checkout for a person who already holds a Stripe membership fails with `BAD_USER_INPUT` and the message "This membership is already paid. Reload the page to continue." ## Connect webhooks and fixtures [#connect-webhooks-and-fixtures] `POST /api/webhooks/stripe` verifies the raw-body signature, refuses live events and requires the connected `event.account`. The account must resolve to a registered local merchant; the historical club comes from subscription/attempt context because merchants can be shared. The server rereads subscription, invoice and lines in that account before projecting the paid period. Duplicate and out-of-order deliveries cannot duplicate membership, shorten access or extend it from an unpaid later period. Transient processing failures remain retryable by Stripe; foreign events do not mutate local business data. Subscription membership payment status is derived from this projected paid period, not from person status. Once the paid-through date passes it reads as `unpaid`. A later valid `invoice.paid` delivery advances the same membership's paid-through date, so its next read returns `paid` automatically. Stripe renews subscriptions itself at each period end; renewals are accepted on the membership's recorded Price, or its scheduled replacement Price, even after the plan's amount changed or its Prices were archived (see [plan splits](/en/graphql/plans#split-a-plan)). This never reactivates a banned, disabled or suspended person; payment eligibility must combine membership billing state with the separate person status. Local stacks use `stripe listen --forward-connect-to`, with isolated listener secrets. Stripe sandbox resources themselves are shared, with no worktree namespace. Bootstrap verifies/reuses the metadata-marked, platform-managed Ukab Clubs account, waits for its card-payment capability, verifies its three Product/Price pairs, Maxime's connected Customer and paid Cercle + Sport subscription, then publishes the validated non-secret `.local/` manifest atomically. The same fixture manifest supports development, staging and production. Without a manifest, seed creates clubs/plans/people but no fabricated Stripe memberships. An invalid supplied manifest fails explicitly. Existing PostgreSQL merchants remain usable without a manifest; Finance creations do not rewrite it. Platform resources remain intact. See [Invoices](/en/graphql/invoices) for club-scoped projections, refresh and credit notes. ## Change a member's payment method [#change-a-members-payment-method] An authenticated member can call `createPaymentMethodPortal(personId: ID!, clubId: ID!)` and receive `{ url }`. The member and club must match the session; staff and applicant access tokens cannot open this portal. The customer must already be associated with the member and the club's connected Stripe account. Champagne never creates a new customer for this action or accepts a customer/account/return URL from the browser. ```graphql mutation PaymentSettings($personId: ID!, $clubId: ID!) { createPaymentMethodPortal(personId: $personId, clubId: $clubId) { url } } ``` The consumer's **Change payment method** button opens Stripe's hosted payment-method update flow. A dedicated connected-account portal configuration disables subscription cancellation, plan changes, profile changes and invoice browsing. The return URL is `CONSUMER_ORIGIN/account/membership?club=`. Portal URLs are temporary credentials and must not be logged or stored. A return to Champagne alone is not evidence that the card was changed. When Stripe cannot open the portal, the mutation returns a generic retry message and the cause is logged with the request ID. Subscribe the connected-account webhook endpoint to `customer.updated` in addition to its existing events. Local worktree listeners include this event. Stripe stores the new customer default payment method. On a signed update of that default, Champagne retrieves the current customer and makes its owned subscriptions in the same club inherit that default, clearing subscription-specific payment overrides and disabling saving invoice payments as subscription-specific defaults. Subscription payment-type overrides are cleared so Stripe can use the newly selected method. Prices and renewal dates are unchanged; no payment or retry of an outstanding invoice is explicitly requested. Only future collections are covered; already-issued invoices can retain their own payment settings. Subscription schedules managed outside Champagne may also override payment settings later. Live mode, missing customer associations, mismatched ownership and provider failures fail closed. Webhook failures return an error so Stripe can retry; repeated or delayed events use the current customer default rather than replaying an old card. Card data is entered only on Stripe. This integration remains sandbox-only like existing billing. Members can also view their [outstanding invoices](/en/graphql/invoices#member-outstanding-invoices) and open the corresponding Stripe payment page from My account. This action is separate from changing the default payment method and does not alter subscription or suspension policies. ## Club credits [#club-credits] See [Club credits](/en/graphql/credits) for credit packs, balances, refill history, credit-priced add-ons and reservations, and cancellation refunds. Membership subscriptions stay billed in money. # Bug reports Submit a bug report to the Champagne GitHub repository. Source: https://docs.champagne.dev-ukab.fr/en/graphql/bug-reports Signed-in staff can use **Settings → Report a bug** to create an issue in `ukab-labs/champagne`. Members and anonymous callers cannot submit reports. Error toast **Report** actions open the same GitHub reporting flow with read-only diagnostics and editable context about what the user was doing. The description includes the error message, available masked backend message, HTTP status, GraphQL code, request ID, timestamp and page origin/path. Query strings, URL fragments, raw stacks, request payloads, credentials and account details are not collected. Development reports prefer the actual Waggle trace link when a valid server trace ID is available; otherwise Compose configures a request-ID-filtered Waggle log link via `VITE_WAGGLE_URL`, using the same host and port as its Caddy receiver. Production reports retain the request ID and timestamp for authorized Cockpit investigation, without assuming a trace exporter or datasource URL. Diagnostics can be reviewed before sending. The form reminds users not to share passwords or personal data. Members and signed-out users can continue with a prefilled GitHub issue draft, which requires their own GitHub access and explicit submission there. This does not grant them access to the staff-only API or expose the server's GitHub token. GitHub draft URLs are limited to 7,000 encoded bytes. Larger drafts contain a bounded summary with correlation information and instructions to paste the full report before submitting. The modal offers **Copy full report** and selectable, read-only full contents; clipboard failures remain inline. This URL limit never truncates the description sent through the staff mutation. ```graphql mutation ReportBug($input: BugReportInput!) { reportBug(input: $input) { url number error } } ``` Pass `input.title` (1–200 characters) and `input.description` (1–10,000 characters). Both are trimmed. Blank fields, null characters, and multiline titles are rejected with `BAD_USER_INPUT`. Staff authentication uses the normal Authorization header. There are no filters, sorting or pagination arguments. Success returns the issue `url` and `number`, with `error: null`. Missing configuration, GitHub rejection, timeouts and malformed provider responses return a safe `error` with null issue fields. The UI preserves the draft on failure and displays the submission error inline, avoiding recursive Report toasts. Pending submission blocks duplicate sends and dismissal. An uncertain network result can mean GitHub created the issue: check the repository before manually retrying. The server does not automatically retry. Each API process limits each staff account to one attempt per minute; this limit is in memory and resets on restart, and is not shared between replicas. ## Configuration [#configuration] Create a fine-grained GitHub personal access token restricted to `ukab-labs/champagne` with **Issues: read and write** permission. If required by the organization, approve the token before use. Set `GITHUB_BUG_REPORT_TOKEN` in the primary checkout’s ignored `.env.local` locally. New worktrees inherit this file at creation; existing worktrees need their own copy of this setting. Restart the API after changing it. Compose loads `.env.local` after `.env`. For Scaleway deployment, supply the sensitive Terraform variable `github_bug_report_token`; it is injected into the backend secret environment. Never put the token in frontend configuration or commit it. Without a token, the rest of the application continues to work; submission returns an explicit configuration error. The issue destination is fixed on the server and cannot be overridden by the caller. Repository access is required to view the resulting issue when the repository is private. # Building entries Let members record when they enter the building and let staff review the entry history. Source: https://docs.champagne.dev-ukab.fr/en/graphql/building-entries Champagne has no badge reader yet. Members record their own entry from the member site with the **I'm in the building** button, and staff review each person's entry history. ## Record an entry [#record-an-entry] ```graphql mutation EnterBuilding($clubId: ID!) { enterBuilding(clubId: $clubId) { error entry { id enteredAt } } } ``` | Argument | Type | Description | | -------- | ----- | ------------------------------------------------ | | `clubId` | `ID!` | Club of the building. Must be the member's club. | Only signed-in **members** of `clubId` can call it, including guests and temporary accounts from online booking. Staff, anonymous callers and members of other clubs receive `UNAUTHENTICATED`. The entry is always recorded for the caller. A member whose status gives no club access (`suspended`, `banned` or `disabled`) gets `entry: null` and the error `Only members with club access can check in.`. Members whose bookings are blocked (`no_booking`) keep club access and can still check in. Pressing the button again within one minute returns the previous entry instead of recording a duplicate. ## Latest entry of the member [#latest-entry-of-the-member] ```graphql query MyLatestBuildingEntry($clubId: ID!) { myLatestBuildingEntry(clubId: $clubId) { id enteredAt } } ``` Returns the caller's most recent entry in `clubId`, or `null` when they never checked in. It follows the same access rules as `enterBuilding`. ## Entries of a person [#entries-of-a-person] ```graphql query PersonBuildingEntries($clubId: ID!, $personId: ID!) { personBuildingEntries(clubId: $clubId, personId: $personId) { id enteredAt } } ``` Returns the 20 most recent entries of `personId` in `clubId`, newest first. It follows the same staff access rules as `person`: the caller needs `members_read` in the person's club (administrators always have it). Other callers receive `UNAUTHENTICATED`. # Club configuration Read and update every club setting in one operation. Source: https://docs.champagne.dev-ukab.fr/en/graphql/club-configuration `clubConfiguration` returns, at once, the settings a club administrator edits in the back office, and `updateClubConfiguration` changes any subset of them atomically. They suit automation such as the [Terraform provider](/en/terraform); the back office keeps using the dedicated operations of each setting, which stay available. Both operations require `settings_manage` in the club (club administrators and [API tokens](/en/graphql/api-tokens) hold it). Other callers receive `UNAUTHENTICATED`. ## Read the configuration [#read-the-configuration] ```graphql query ClubConfiguration($clubId: ID! = "51000000-0000-4000-8000-000000000051") { clubConfiguration(clubId: $clubId) { clubId name description timezone hostedSlug address { line1 line2 postalCode city state country } billingEnabled credits { creditName creditsPerEuro lowBalanceThreshold } reservationGuestsRequireAccount depositRefundDays noShowSanctions { resetAfterDays rules { noShows action suspensionDays } } } } ``` Returns null when the club does not exist. `name` is read-only here: only platform administrators rename a club. `billingEnabled` is false when the platform turned the club's `billing` module off. ## Update the configuration [#update-the-configuration] ```graphql mutation UpdateClubConfiguration($input: ClubConfigurationInput!) { updateClubConfiguration(input: $input) { clubId timezone hostedSlug } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "timezone": "Europe/Paris", "hostedSlug": "ukab-clubs", "depositRefundDays": 7, "noShowSanctions": { "resetAfterDays": 90, "rules": [{ "noShows": 3, "action": "suspend", "suspensionDays": 14 }] } } } ``` Only `clubId` is required. A field left out keeps its value. `description`, `hostedSlug` and `address` are cleared by an explicit `null`; on any other field `null` also keeps the value. Every provided setting is applied in one transaction: when one is refused, none is saved. The mutation returns the configuration after the update. | Field | Validation | | --------------------------------- | ------------------------------------------------------------------------------------------- | | `description` | Trimmed, at most 2,000 characters; blank is stored as null. | | `timezone` | A supported IANA zone or `UTC`. | | `hostedSlug` | 3–63 lowercase letters, digits or hyphens, not reserved, unique across clubs. | | `address` | Same rules as [`updateClubAddress`](/en/graphql/people#update-a-club-address). | | `credits` | Same rules as [`configureCredits`](/en/graphql/credits); refused when billing is off. | | `reservationGuestsRequireAccount` | Boolean. | | `depositRefundDays` | 0 to 365. | | `noShowSanctions` | 1 to 3,650 reset days, at most 10 rules with distinct `noShows`; rules are returned sorted. | Invalid values and refusals return `BAD_USER_INPUT` with a message; an unknown club returns `This club is unavailable.` Each group of settings that actually changes is recorded in the club activity under the action of its dedicated mutation. # Club credits Credit settings, one-time refills, wallets and purchases. Source: https://docs.champagne.dev-ukab.fr/en/graphql/credits Stripe provider failures return a masked `INTERNAL_SERVER_ERROR` with a server-generated `extensions.requestId`. Use that ID to correlate the sanitized server diagnostic. Expected business validation errors keep their public codes; provider messages and payloads are never returned to clients. Club memberships remain billed in money. Reservation and add-on `amount` fields are prices in hundredths of a club credit; credit-pack `amount` fields are euro cents. `credit_pack` plans are one-time top-ups, never subscriptions. Configure packs in Plans and the credit name and conversion rate in club settings. A club with the `billing` module turned off refuses top-ups, credit settings and every credit debit with `BAD_USER_INPUT` ("Payments are turned off for this club."). Balances, history and refunds keep working; see [Clubs with billing turned off](/en/graphql/platform#clubs-with-billing-turned-off). All credit amounts are integers in hundredths, including balances and the `creditsPerEuro` setting. For example, `creditsPerEuro: 1000` means €1 buys 10 credits at the standard rate. Every pack sets its own `creditAmount`: a €100 pack (`amount: 10000`) can grant 1,250 credits (`creditAmount: 125000`). Pack quantities must be integers from 1 through 100,000,000 units. The consumer displays savings against the standard rate: `max(0, floor(100 - amount * creditsPerEuro / creditAmount))`. This pack saves 20%. The frontend calculates this display only; the server snapshots and grants the exact stored pack quantity. Rate changes never alter pack quantities. ## Settings [#settings] `creditSettings(clubId)` returns `creditName`, `creditsPerEuro` and `lowBalanceThreshold`. Members can read settings for their club, as can every staff account of the club; `configureCredits` requires `settings_manage` (club administrators included; see [staff permissions](/en/graphql/auth#individual-staff-permissions)). `configureCredits` accepts a trimmed name of 1–40 characters without control characters, an integer rate of 1–1,000,000 (0.01–10,000 credits per euro), and a `lowBalanceThreshold` in euro cents from 0 through 100,000,000 (default `1000`, €10). ## Low balance alerts [#low-balance-alerts] When a debit (reservation, deposit or add-on) moves a wallet from at or above the threshold to below it, the member receives a `credits` notification. The euro threshold is converted at the club's current rate: the balance is low when `balance * 100 < lowBalanceThreshold * creditsPerEuro`. One alert is queued per crossing; further debits below the threshold do not repeat it, and a refill above the threshold rearms it. `0` disables alerts. The email is skipped if the balance is no longer low when the worker claims it, and the member's `credits` notification preferences apply. ```graphql mutation { configureCredits( clubId: "51000000-0000-4000-8000-000000000051" creditName: "Stars" creditsPerEuro: 1000 lowBalanceThreshold: 1000 ) { creditName creditsPerEuro lowBalanceThreshold } } ``` ## Balance and history [#balance-and-history] `creditWallet(clubId, personId, page: 1)` returns `balance`, `creditName`, `creditsPerEuro`, `entries` and `hasMore`. Only the person or staff with `members_read` in that person's club (club administrators included) can read it. The person must belong to the requested club. New wallets have a zero balance. History pages have 20 entries; `page` is 1–10,000. Ordering is newest `createdAt` first, then descending `id`. There are no filter or sort arguments. Each entry has `id`, signed `amount`, resulting `balance`, `kind` (`refill`, `reservation`, `deposit`, `refund`, `refund-deposit`, `addon`; `refund-deposit` is a returned reservation deposit, `refund` a refunded price), `description`, `paidAmount` (euro cents for refills, otherwise null), the name used for that transaction (`creditName`), and ISO `createdAt`. ```graphql query { creditWallet( clubId: "51000000-0000-4000-8000-000000000051" personId: "51000000-0000-4000-8000-000000000013" page: 1 ) { balance creditName hasMore entries { id kind amount balance paidAmount description creditName createdAt } } } ``` ## One-time top-ups [#one-time-top-ups] `createCreditCheckout(clubId, personId, planId, requestId)` returns a `CreditCheckout` with `url: String` and `sessionId: String!`. IDs must be UUIDs. Choose an active `credit_pack` from `plans(clubId)`. The pack must be priced in EUR with a positive amount and credit quantity and belong to the club's configured merchant. The person must be active and belong to the club. Members can purchase only for themselves. Guests and temporary accounts from [external reservations](/en/graphql/external-reservations) receive `UNAUTHENTICATED`, and the purchase itself refuses any person with a guest access grant (`Guests cannot purchase credits.`). Keep the same request ID when retrying an uncertain request. The server snapshots the pack, euro price, credit name and credit quantity when reserving the purchase. Settings, quantity or price changes do not change an existing checkout. The server reuses an open session or replaces an expired session with an idempotent replacement key. Canceling an unpaid checkout grants no credits. When Checkout is complete or the purchase is already credited, `url` is `null`: do not open another payment. Query `myCreditTopUpStatus` for that request instead. Checkout completion alone does not prove that the wallet has been credited. When the complete session is paid but its webhook was missed, this call credits the purchase itself, exactly once. ```graphql mutation PrepareTopUp($clubId: ID!, $personId: ID!, $planId: ID!, $requestId: ID!) { createCreditCheckout( clubId: $clubId personId: $personId planId: $planId requestId: $requestId ) { url sessionId } } ``` A signed webhook and a verified, paid Stripe Checkout session from the expected connected account grant the refill. The session must match the snapshotted amount, EUR currency and purchase metadata. Its payment intent ID is retained without a second PaymentIntent retrieval. Returning from Checkout does not itself grant credits. Duplicate webhooks cannot credit a purchase twice. The member UI retains each pending `requestId` per club, member and pack in the browser session and sends one checkout mutation per attempt. It removes the saved request only after that purchase is confirmed as credited, allowing a new purchase. The success return URL includes the original `requestId`. Members query `myCreditTopUpStatus(clubId, requestId)` for that exact purchase. Both IDs must be UUIDs; the authenticated member and club scope are enforced. An unknown or other member's request returns `null`. The result exposes `completed`, `credits` (hundredths) and `creditName`. `completed` becomes true only when fulfillment has atomically credited the wallet. The member UI polls until completion, shows **Credits added.**, and refreshes the wallet. A different refill or refund never marks this request as completed. ```graphql query TopUpStatus($clubId: ID!, $requestId: ID!) { myCreditTopUpStatus(clubId: $clubId, requestId: $requestId) { completed credits creditName } } ``` Account eligibility for credit purchases, reservation payments and add-on purchases uses the effective status on today’s UTC date. Future and elapsed suspensions do not block these operations; a suspension blocks them on both boundary dates and every day between. Banned and disabled accounts remain blocked. This does not change the processing of an already paid top-up or reservation refunds. ## Spend and cancel [#spend-and-cancel] `payReservationCredits(clubId, reservationId, paymentVersion)` and `payAddonCredits(clubId, planId)` return `true`. Both derive the person from the signed-in member, enforce club ownership and require an active account. If the wallet cannot cover the debit, these mutations return a GraphQL error with `extensions.code: "INSUFFICIENT_CREDITS"`. Clients should select their localized top-up guidance using this code, never by comparing `message`. Other credit validation errors retain `BAD_USER_INPUT`; unexpected technical errors remain masked. A rejected debit leaves the wallet unchanged. Reservations must be booked and awaiting payment. Send the `paymentVersion` UUID from the displayed reservation. A changed version or outstanding proposal is rejected before any debit; refresh and ask for explicit payment again. Successful retries remain idempotent. The transaction debits the quoted reservation price and security deposit together, confirms the reservation and queues its existing confirmation notification. Price and deposit are separate ledger entries (`reservation` and `deposit`) in the same transaction. Insufficient funds for their total rolls back both debits. A zero-price booking still requires payment if it has a deposit. An already paid reservation is idempotent. Add-ons use the current active club plan price and grant the existing membership record. Already owned add-ons do not debit again. Balances cannot go negative, including when two different purchases run concurrently. Cancellation through the existing member or staff reservation mutation, including `cancelReservationSession` for all bookings in an occurrence, refunds exactly the credits debited, once. A member cancellation within the resource's [cancellation notice](/en/graphql/reservations#cancellation-notice) refunds only the collected deposit. Exchange-rate or credit-name changes do not alter that refund. After departure, the collected deposit is refunded once, in full or in part (see [Departure and deposit refunds](/en/graphql/reservations#departure-and-deposit-refunds)); the price remains charged. Pending or unpaid reservations do not produce a refund. All refunds appear as `refund` entries in credit history. Changing the window, resource, or products of a paid reservation through `updateReservation` or `updateMyReservation` debits or refunds the price difference as a single entry described as ` · Reservation change` (or `· Products` when only products changed); see [Rescheduling a paid reservation](/en/graphql/reservations#rescheduling-a-paid-reservation). Cancellation then refunds the net amount debited. `createReservationCheckout` and `createAddonCheckout` reject new cash purchases from members and direct them to credits. Only temporary accounts created by [external reservations](/en/graphql/external-reservations) pay their booking by card. Existing cash-payment webhook and refund processing remains available for outstanding payments. # Customer chats Let members and staff start conversations on any subject, outside of a reservation. Source: https://docs.champagne.dev-ukab.fr/en/graphql/customer-chats A customer chat is a conversation between one person of the club (a member or an applicant) and the club team about a subject of their choice. Either side can start it, and a person can keep several chats open at once. Reservation conversations stay attached to their reservation (see [Reservations](/en/graphql/reservations)). Staff need the `customer_chats` permission in the club; club administrators always have it. Members can only reach their own chats in the club of their session. ## Staff inbox [#staff-inbox] ```graphql query CustomerChats($clubId: ID!, $archived: Boolean!) { customerChats(clubId: $clubId, archived: $archived) { id personId personName subject createdAt archivedAt unread lastMessage { author senderName body createdAt } } } ``` | Argument | Type | Description | | ---------- | ---------- | --------------------------------------------------------------- | | `clubId` | `ID!` | Club whose chats are listed. | | `archived` | `Boolean!` | `false` (default) lists active chats, `true` the archived ones. | Every staff member with access sees every chat of the club, sorted by latest message first. `unread` and `archivedAt` belong to the caller: a chat is unread while a customer message is newer than the caller's last read, and archiving it never affects another staff member. Any new message, from either side, restores the chat to every staff member's active list. ```graphql query CustomerChat($clubId: ID!, $chatId: ID!) { customerChat(clubId: $clubId, chatId: $chatId) { id personId personName subject messages { id author senderName body createdAt } } } mutation ReadCustomerChat($clubId: ID!, $chatId: ID!) { markCustomerChatRead(clubId: $clubId, chatId: $chatId) } mutation ArchiveCustomerChat($clubId: ID!, $chatId: ID!) { setCustomerChatArchived(clubId: $clubId, chatId: $chatId, archived: true) } ``` `customerChat` returns `null` for a chat of another club. Messages are ordered oldest first; `author` is `customer` or `professional`. `markCustomerChatRead` and `setCustomerChatArchived` return `false` when the chat does not exist in the club. ## Start and answer as staff [#start-and-answer-as-staff] ```graphql mutation StartCustomerChat($clubId: ID!, $personId: ID!, $subject: String!, $body: String!) { startCustomerChat(clubId: $clubId, personId: $personId, subject: $subject, body: $body) { id subject } } mutation PostCustomerChatMessage($clubId: ID!, $chatId: ID!, $body: String!) { postCustomerChatMessage(clubId: $clubId, chatId: $chatId, body: $body) { id messages { id body } } } ``` The person must belong to `clubId`. Messages are signed with the staff member's name, or `Club team` when it is empty, and are immediately read for the sender. Each staff message sends the member a `chat` notification in the app and by email (according to their preferences) linking to `/account/chats/{chatId}`, and emits the `chat.message` webhook. Starting a chat is recorded as `customer_chat.started` and later replies as `customer_chat.message_posted` in the staff activity log. ## Member operations [#member-operations] ```graphql query MyCustomerChats($clubId: ID!) { myCustomerChats(clubId: $clubId) { id subject unread lastMessage { author senderName body createdAt } } } mutation StartMyCustomerChat($clubId: ID!, $subject: String!, $body: String!) { startMyCustomerChat(clubId: $clubId, subject: $subject, body: $body) { id } } ``` `myCustomerChat(clubId, chatId)`, `postMyCustomerChatMessage(clubId, chatId, body)` and `markMyCustomerChatRead(clubId, chatId)` complete the member API. A member chat is unread while a professional message is newer than the member's last read; the member's own messages count as read. Chats of another member return `null` (or `false` for `markMyCustomerChatRead`). Members of another club and staff accounts receive `UNAUTHENTICATED`. Customer messages are signed with the member's name and do not notify staff beyond the unread state of their inbox. ## Validation [#validation] Subjects are trimmed and must contain 1 to 120 characters; message bodies are trimmed and must contain 1 to 4000 characters. Invalid IDs and out-of-range values return `BAD_USER_INPUT`. # External reservations Let non-members book exposed resources online with a temporary account and pay by card. Source: https://docs.champagne.dev-ukab.fr/en/graphql/external-reservations A reservation category can open some of its resources to people who are not club members. On the public member site (`/book/`), a visitor chooses a category, a resource, the number of people (only for resources holding more than one) and a period, then either signs in as a member or books without a membership. Booking without a membership creates a **temporary account** that ends at the close of the reservation's last day and pays the price and security deposit by card through Stripe Checkout. ```mermaid sequenceDiagram participant V as Visitor participant A as Champagne API participant S as Stripe V->>A: externalReservationCatalog / externalReservationAvailability V->>A: createExternalReservation A-->>V: token + checkoutUrl (reservation booked) V->>S: Pay price + deposit S->>A: checkout.session.completed A-->>A: reservation confirmed, deposit marked paid Note over A: End of last day: temporary account deleted ``` Members who sign in on the same page book through [`createReservation`](/en/graphql/reservations#request-a-reservation) as usual: they pay with credits at their membership price, and still need an active membership. ## Configuration [#configuration] * `ReservationCategoryInput.externalBookingEnabled` (default `false`) turns the module on. It cannot be combined with `conversationRequired` or `requiresSession`; the mutation returns `BAD_USER_INPUT` (`External booking cannot be combined with required conversations or sessions.`) and a database check also refuses the combination. * `UpdateReservationResourceInput.externalBookable` exposes one resource. Omitting it keeps the current value. * `UpdateReservationResourceInput.externalAmount` is the non-member price in cents per `priceUnit`, from 0 to 100,000,000. It is required when `externalBookable` is true (`BAD_USER_INPUT`, `Enter the price for non-members.`), because member prices are in credits. It is always written, so omitting it clears it, like `depositAmount`. `ReservationCategory.externalBookingEnabled`, `ReservationResource.externalBookable` and `ReservationResource.externalAmount` return the stored values. External bookings follow the category's [booking rules](/en/graphql/reservations#booking-rules-and-time-slots): opening hours, booking window and minimum notice. When the category sets `defaultDurationMinutes`, exposed in the catalog, the end is the start plus that length and the booking page hides the end fields. Slot booking cannot be combined with external booking. ## Clubs open to online booking [#clubs-open-to-online-booking] ```graphql query ExternalBookingClubs { externalBookingClubs { id name } } ``` The query is public. It lists, by name then ID, the clubs with at least one category open to external booking that has an `externalBookable` resource with an `externalAmount`. The member site's **Book online** navigation tab uses it. ## Catalog [#catalog] ```graphql query ExternalCatalog($clubId: ID!) { externalReservationCatalog(clubId: $clubId) { clubId clubName timezone categories { id name description currency defaultDurationMinutes resources { id categoryId name amount priceUnit depositAmount maxOccupants cancellationNoticeDays } } } } ``` The query is public. It returns `null` for an unknown club. Only categories with `externalBookingEnabled` that have at least one `externalBookable` resource with an `externalAmount` are listed, ordered by name then ID, and only those resources are included. `amount` is the non-member price (`externalAmount`). `depositAmount` is the resource's security deposit, set in credits, converted to euro cents at the club's `creditsPerEuro` rate. ## Availability [#availability] ```graphql query ExternalAvailability($clubId: ID!, $categoryId: ID!, $from: String!, $to: String!) { externalReservationAvailability(clubId: $clubId, categoryId: $categoryId, from: $from, to: $to) { busyPeriods { resourceId startsAt endsAt } blocks { resourceId startsAt endsAt } } } ``` The query is public. `from` and `to` are UTC ISO timestamps at most 62 days apart; other values return `BAD_USER_INPUT`. A category that is not open to external booking returns empty lists. Busy periods carry no customer information. A block with a null `resourceId` covers the whole category. ## Book without a membership [#book-without-a-membership] ```graphql mutation CreateExternalReservation($input: CreateExternalReservationInput!) { createExternalReservation(input: $input) { token personId reservationId checkoutUrl error } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "categoryId": "73000000-0000-4000-8000-000000000001", "resourceId": "79000000-0000-4000-8000-000000000001", "startsAt": "2026-11-02T14:00:00.000Z", "endsAt": "2026-11-04T10:00:00.000Z", "firstName": "Iris", "lastName": "Visitor", "email": "iris@example.com", "phoneNumber": "+33 6 12 34 56 78", "password": "a long password", "guests": 2 } } ``` | Argument | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------------- | | `clubId` | `ID!` | Club of the category. | | `categoryId` | `ID!` | Category with `externalBookingEnabled`. | | `resourceId` | `ID!` | An `externalBookable` resource of that category. | | `startsAt` | `String!` | UTC ISO start, in the future. | | `endsAt` | `String!` | UTC ISO end, after the start and at most 31 days later. | | `firstName` | `String!` | 1-100 characters after trimming. | | `lastName` | `String!` | 1-100 characters after trimming. | | `email` | `String!` | Trimmed and compared case-insensitively. | | `phoneNumber` | `String!` | 6-30 digits, spaces, `+`, `(`, `)`, `.` or `-`. | | `password` | `String!` | 12-200 characters with a lowercase letter, an uppercase letter, a digit and a symbol. | | `guests` | `Int!` | Number of people including the booker, default 1. | The mutation is public. Malformed IDs or timestamps return `BAD_USER_INPUT`. Business refusals return a result with every other field null and a user-facing `error`, without creating anything: * the category is not open to online booking, or the resource is not exposed; * the period is in the past, inverted, longer than 31 days, blocked or already reserved; * `guests` is above the resource's `maxOccupants` (`Too many people for this resource.`); values outside 1-500 return `BAD_USER_INPUT`; * the email already has an account in this club (`This email already has an account in this club. Sign in instead.`). An expired temporary account with that email is deleted and replaced; * the club has no Stripe merchant configured for payments. On success, one transaction creates the person, its credential, a `guest_access` grant of kind `external`, a session and the reservation, then Champagne opens a Stripe Checkout session for `amount + depositAmount`. The reservation is `booked` until the payment completes, or `confirmed` straight away when nothing is due. `token` signs the visitor in as the temporary account. `checkoutUrl` is the Stripe page; it is null when nothing is due, or when Stripe could not be reached, in which case `error` asks the visitor to pay from their account. The reservation price uses the resource's non-member price and price unit. The deposit is the resource's `depositAmount`, set in credits and converted to euro cents at the club's `creditsPerEuro` rate when the booking is made. The booking snapshots the contact details in `Reservation.externalCustomer` and sets `Reservation.source` to `external`. ## Card payment [#card-payment] `createReservationCheckout(clubId, reservationId, requestId)` returns the open Stripe Checkout session of a `booked` reservation for the signed-in temporary account that owns it, or creates a new one. Regular members receive an error because they pay with credits. When the last checkout is already paid but its `checkout.session.completed` webhook has not been processed yet, no new session is created: the reservation is confirmed from the paid session, as the webhook would, and the mutation returns the error `"Your payment was received. Your reservation is being confirmed."`. A checkout expires after 30 minutes. When Stripe reports `checkout.session.expired`, the still-unpaid reservation is canceled and its period released. `checkout.session.completed` confirms it and marks the deposit as paid. Canceling an unpaid reservation with `cancelReservation`, `updateReservation` or `cancelMyReservation` expires its open checkout; the cancellation still succeeds if Stripe cannot be reached. If the reservation was canceled before the payment completed anyway, the webhook refunds the price and deposit in full instead of confirming it, and records the payment and refund on the canceled reservation. Refunds go back to the card: * `cancelMyReservation` by the temporary account refunds the price and deposit, or only the deposit once the cancellation notice has passed. It is refused once the reservation has started. * Staff canceling with `cancelReservation` or `updateReservation` refund the price and deposit. Once the cancellation notice has passed, only the deposit is refunded unless they pass `refundLateCancellation: true`. * After departure, staff refunding the deposit, or the automatic refund, return all or part of it (see [Departure and deposit refunds](/en/graphql/reservations#departure-and-deposit-refunds)). ## Temporary accounts [#temporary-accounts] `memberViewer.external` is `true` for temporary accounts and `guestExpiresAt` is the end of their access: the next club-local midnight after the reservation ends. `PersonDetail.guestAccess.invitedBy.kind` is `external`, with a null inviter. Like guests, temporary accounts cannot buy memberships or add-ons. They also cannot buy or spend credits, create or change reservations, join waitlists, invite people or open the payment-method portal (`UNAUTHENTICATED`). They can read their account and reservations, download tickets, chat, cancel and pay by card. Every five minutes a worker deletes temporary accounts whose access has ended. Their sessions, credential, access grant and other personal records are deleted with the person. Unpaid reservations are canceled first. Reservations keep `guestName`, `source` and the `externalCustomer` contact details as the booking record. Three years after a reservation ends, the worker anonymises it: `externalCustomer` becomes null and `guestName` becomes `External customer`. # Guests Invite guests by email and give them a temporary member account for 12, 24 or 48 hours. Source: https://docs.champagne.dev-ukab.fr/en/graphql/guests Club administrators and members can invite a guest by email. When the guest accepts, Champagne creates a temporary member account in the inviter's club that stays active for the duration the inviter chose: 12, 24 or 48 hours. 1. The host calls `inviteGuest(clubId, email, durationHours)`. 2. Champagne emails the guest a `/guest-invitation?token=…` link on the member site. 3. The guest previews it with `guestInvitation(token)` and accepts with `acceptGuestInvitation`. 4. Acceptance creates a `guest_access` grant and signs the guest in until that grant expires. ## Invite a guest [#invite-a-guest] ```graphql mutation InviteGuest($clubId: ID!, $email: String!, $durationHours: Int!) { inviteGuest(clubId: $clubId, email: $email, durationHours: $durationHours) } ``` ```json { "clubId": "51000000-0000-4000-8000-000000000051", "email": "guest@example.com", "durationHours": 24 } ``` | Argument | Type | Description | | --------------- | --------- | --------------------------------------------------------------- | | `clubId` | `ID!` | Club the guest joins. Must be the inviter's club. | | `email` | `String!` | Guest email. Trimmed and compared case-insensitively. | | `durationHours` | `Int!` | Guest access granted on acceptance: exactly `12`, `24` or `48`. | Callers must be either a staff account with `members_manage` in `clubId` (club administrators hold every permission; see [staff permissions](/en/graphql/auth#individual-staff-permissions)) or a signed-in **member** of that club whose effective status is `active`. Staff without that permission, guests, anonymous callers and other clubs receive `UNAUTHENTICATED`. Other durations, invalid emails and emails that already belong to a regular person of the club fail with `BAD_USER_INPUT`. An email belonging to a current or former guest can be invited again. The mutation returns `true` only after the email was delivered. The email contains a single-use link to `CONSUMER_ORIGIN/guest-invitation?token=…`, valid for 7 days. The guest access duration only starts when the guest accepts. Inviting the same email again from the same inviter replaces their unaccepted invitation. If delivery fails, the new invitation is removed and a retryable error is returned. No token is returned to the caller. ## List invitations [#list-invitations] ```graphql query GuestInvitations($clubId: ID!) { guestInvitations(clubId: $clubId) { id email durationHours status invitedBy guestName createdAt expiresAt accessExpiresAt } } ``` Administrators see every invitation of the club; members see only the invitations they sent. Results are ordered newest first and limited to 100. | Field | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------------------------- | | `status` | `GuestInvitationStatus!` | `pending`, `expired` (link unused and elapsed), `active` or `ended`. | | `invitedBy` | `String!` | Member full name or staff name of the inviter. | | `guestName` | `String` | Guest full name once accepted. | | `expiresAt` | `String!` | ISO timestamp after which the link can no longer be accepted. | | `accessExpiresAt` | `String` | ISO end of the temporary account granted by this invitation. | An accepted invitation is `active` while its access grant is neither expired nor revoked, and `ended` afterwards. ## Accept an invitation [#accept-an-invitation] The public `guestInvitation` query previews a link before acceptance. It returns `null` for unknown tokens; `expired` is `true` once the link was used or has elapsed. ```graphql query GuestInvitationPreview($token: String!) { guestInvitation(token: $token) { email clubName invitedBy durationHours expired } } mutation AcceptGuestInvitation( $token: String! $firstName: String! $lastName: String! $password: String! ) { acceptGuestInvitation( token: $token firstName: $firstName lastName: $lastName password: $password ) { token personId clubId } } ``` Names need 1–100 characters and the password follows the member password policy. Acceptance atomically creates the guest person and login (or renews a former guest with the same email), creates a `guest_access` row ending at now plus `durationHours`, consumes the link, revokes the guest's older sessions and returns a member session. Used, expired or unknown links fail with `BAD_USER_INPUT`, as does an email that became a regular member of the club in the meantime. ## Guest accounts [#guest-accounts] Guests are ordinary people whose latest `guest_access` grant determines their temporary access. The derived `guestExpiresAt` field remains exposed on `Person`, `PersonDetail` and `memberViewer`; `PersonDetail.guestAccess` also exposes the latest grant and its structured inviter. Until expiry, a guest can sign in with `loginMember`, read their own profile and see the reservations they are invited to. Guests do not book themselves: the member site offers them no club schedule. They cannot start membership, add-on or credit Checkout and cannot invite other guests. The member site shows them only Overview, Reservations (the reservations they were invited to) and Notifications. Once `guestExpiresAt` has passed, existing sessions stop resolving and sign-in fails with `Your guest access has ended.` The person and access records are kept for history. If an inviter account is deleted, the access record retains its inviter kind while the inviter ID and name become null; clients should display a deleted-user label. ## Reservation invitations requiring accounts [#reservation-invitations-requiring-accounts] Clubs can require accounts for reservation guests in admin Settings. See [Reservation guest accounts](/en/graphql/reservations#require-guest-accounts) for the policy queries and invitation arguments. These invitations appear in `guestInvitations` and use the same `guestInvitation` preview and `acceptGuestInvitation` registration contract. Acceptance also links the person to the reservation and marks their invitation accepted in the same transaction. A reservation invitation can complete an existing person record that has no login credentials; it never overwrites an existing member password. Invalidated reservation links cannot register. Sending a regular Guests-tab invitation does not invalidate invitations tied to reservations. # Overview Explore the public GraphQL endpoint, available queries, and generated contract. Source: https://docs.champagne.dev-ukab.fr/en/graphql The GraphQL endpoint is `/api/graphql`. It accepts queries over GET or POST and mutations over POST. Stripe delivers signed invoice/payment/credit-note webhooks separately to `/api/webhooks/stripe`. Invoice queries read only PostgreSQL; automatic resumable import and webhook jobs maintain the projection. The invoice list exposes synchronization progress separately from local totals. GraphiQL is available at that endpoint in development, not production. Floor-plan image imports use `startFloorPlanImport`, `floorPlanImport` and `cancelFloorPlanImport` for user/club/category-scoped draft jobs. They require `catalog_manage` and expose deterministic native geometry, original-source crop/registration, regional reference/draft inspection and bounded name-based templates. Booking bindings are explicit and independent of visuals; labels never create resources. Early/periodic/final inspections and honest review notes precede human acceptance, without claiming a PNG proves fidelity. Saving remains explicit through the existing floor-plan mutation; see [Floor plan imports](/en/graphql/reservations#floor-plan-imports). Floor-plan elements support optional `chairPositions`, normalized perimeter fractions for individually positioned attached chairs. Null retains automatic placement; saves validate counts and reject object/chair collisions or insufficient safe space. See [Reservations](/en/graphql/reservations) for the input/output contract. Artwork generation and brush editing support per-request tracing options, an allowlisted Sunburst/Flare generation model (Sunburst by default), and a separate Gemini brush model. Brush classification defaults to DeepSeek + Gemini; the request-local `classifyBrush: false` option skips DeepSeek and lets Gemini infer and edit together, with the same required rendered visual review. The scoped `floorPlanArtworkProgress` query provides lightweight live milestones without image or prompt payloads; see [Reservations](/en/graphql/reservations#live-artwork-progress) for defaults, authorization, polling and failure semantics. [Bug reports](https://docs.champagne.dev-ukab.fr/en/graphql/bug-reports) Submit staff bug reports to the Champagne GitHub repository. [Authentication](https://docs.champagne.dev-ukab.fr/en/graphql/auth) Sign members in per club, choose a password after onboarding, create people, and sign staff in across clubs. [Guests](https://docs.champagne.dev-ukab.fr/en/graphql/guests) Invite guests by email for a temporary 12, 24 or 48 hour member account. [Building entries](https://docs.champagne.dev-ukab.fr/en/graphql/building-entries) Record member entries into the building and review each person's entry history. [People selection](https://docs.champagne.dev-ukab.fr/en/graphql/people) Select individuals or enterprises with type, search, club, membership, age, and gender filters, fixed sorting, pagination, and details. Edit legal status with enterprise SIRET and TVA. [Member attributes](https://docs.champagne.dev-ukab.fr/en/graphql/person-attributes) Define typed club attributes, save onboarding answers to them, and edit or filter them on people. [Sandbox membership billing](https://docs.champagne.dev-ukab.fr/en/graphql/billing) Compare three annual IÉNA offers and start Stripe-hosted subscription Checkout. [Merchant profiles](https://docs.champagne.dev-ukab.fr/en/graphql/merchants) Select, create, activate and prepare a connected sandbox merchant for a club. [Plans](https://docs.champagne.dev-ukab.fr/en/graphql/plans) List subscription and add-on plans with amounts, currencies, and intervals. [Reservations](https://docs.champagne.dev-ukab.fr/en/graphql/reservations) Request reservations as a member; manage types and browse reservations as staff. [Notifications](https://docs.champagne.dev-ukab.fr/en/graphql/notifications) Read member notifications and configure in-app and email preferences per category. [Invoices](https://docs.champagne.dev-ukab.fr/en/graphql/invoices) Browse sandbox invoices, payments and PDFs, and issue full credit notes. [Applications](https://docs.champagne.dev-ukab.fr/en/graphql/applications) Start, advance, inspect, and review immutable onboarding submissions. [Onboarding](https://docs.champagne.dev-ukab.fr/en/graphql/onboarding) List, create, read, and edit a club's audience-specific onboardings. ## Available queries [#available-queries] | Query | Purpose | | --------------------------- | ---------------------------------------------------------------------- | | `myNotifications` | Paginate the calling member's inbox and unread count within a club. | | `myNotificationPreferences` | Read the calling member's per-category delivery preferences. | | `memberViewer` | Return the calling member's person, club and guest expiry, or null. | | `guestInvitations` | List a club's guest invitations, or the calling member's own. | | `guestInvitation` | Preview a guest invitation link before accepting it. | | `staffViewer` | Return the calling staff user's clubs, or null. | | `peoplePage` | Select a filtered, paginated list of people and its total count. | | `person` | Fetch one person's details by ID, optionally scoped to a club. | | `clubDirectory` | Find clubs for the people selection's club filter. | | `clubWebhookEndpoints` | List the selected club's outbound webhook endpoints (admin). | | `clubWebhookEventCatalog` | List implemented notification types and descriptions (admin). | | `people` | Fetch up to 100 demo people without filtering or pagination. | | `plans` | Fetch active plans globally or through a club's selected merchant. | | `reservationCategories` | List a club's persisted reservation categories. | | `reservationAvailability` | List session occurrences and blocked periods for a type and UTC range. | | `reservations` | Paginate reservations filtered by club and type. | | `reservation` | Fetch one staff-visible reservation. | | `myReservation` | Fetch one reservation owned by the authenticated member. | | `reservationChat` | Read a staff-visible reservation conversation. | | `reservationChats` | List the staff account's active or archived chats and unread state. | | `myReservationChat` | Read the authenticated member's own reservation conversation. | | `hostedClub` | Resolve the public club identity associated with a hosted slug. | | `invoices` | Filter and paginate eligible sandbox subscription invoices. | | `invoice` | Retrieve an invoice, its payments, taxes, lines and credit notes. | | `onboarding` | Fetch one club onboarding by ID. | | `onboardings` | List the onboardings configured for a club. | | `applications` | List a club's applications. | | `application` | Retrieve an application snapshot for back-office review. | | `applicationHistory` | Read a club application’s recorded status transitions and attribution. | | `membershipOffers` | Load club-scoped sandbox offers with explicit readiness and reason. | | `merchantProfiles` | List same-space merchants and the club's selected local profile ID. | | `membershipCheckoutStatus` | Confirm the returned session's subscription for its person and club. | | `addonOffers` | Load club-scoped one-time add-on offers with readiness and reason. | | `addonCheckoutStatus` | Confirm the returned session's one-time add-on payment. | The [people selection reference](/en/graphql/people) documents arguments, return fields, validation, and selection behavior, including the `updatePerson` mutation for personal details, `updatePersonNote` for saving or clearing a person's note, `updatePersonStatus` for suspension, bans, disablement, and reactivation, and `createPerson` for staff-created people. The [authentication reference](/en/graphql/auth) documents member and staff sign in, password choice after onboarding, person creation, and account activation with `activateMemberAccount` and `resendPersonActivation`. The [guests reference](/en/graphql/guests) documents `inviteGuest` and `acceptGuestInvitation`. The [plans reference](/en/graphql/plans) documents club-scoped listing and Stripe-synchronized create, update and delete mutations. Onboarding mutations are also available. Mutations use POST. Person management requires staff authentication and authorized club access. People have a `PersonType` (`individual` or `entreprise`) and a `PersonStatus` (`active`, `suspended`, `no_booking`, `banned`, or `disabled`); enterprise profiles can also include optional SIRET and TVA identifiers. The [billing reference](/en/graphql/billing) documents `createMembershipCheckout(personId: ID!, clubId: ID!, planId: ID!, requestId: ID!)` and `createAddonCheckout(personId: ID!, clubId: ID!, planId: ID!, requestId: ID!)`. The [merchant reference](/en/graphql/merchants) documents selection, creation, activation, verification and catalogue preparation. The [invoice reference](/en/graphql/invoices) documents `refreshInvoice`, `creditInvoice` and invoice queries, all requiring club context. These operations enforce authenticated club access. The [webhook endpoints reference](/en/graphql/webhooks) covers admin-only management of a club's endpoints: adding, pausing and deleting them, their signing secrets and rotation. The [reservation reference](/en/graphql/reservations) documents member-only `createReservation(input: CreateReservationInput!)`, which creates a pending request using the signed-in member's identity, plus connected-account Checkout for paid bookings. Members can list their club's reservation categories; type management, direct staff booking, reservation editing, and professional messages remain staff-only. Admins also manage weekly/custom sessions and blocked periods. Members can edit pending requests, cancel their own requests, accept staff-proposed times, and access only conversations they own. ## Run a query [#run-a-query] Click the pencil on the example to edit it in Monaco, then select **Run query**. Results come from the live API. Expand **Variables** to supply a JSON object. ```graphql query People { people { id firstName lastName email } } ``` The query returns up to 100 records from `public.person`, ordered by first name, last name, and ID. Compose seeds fictional examples, including: ```json { "id": "00000000-0000-4000-8000-000000000001", "firstName": "Alex", "lastName": "Morgan", "email": "alex@example.com" } ``` The GraphQL response wraps the records in `{"data":{"people":[...]}}`. Set `APP_URL` to the URL printed by `make dev` or `make worktree-up`: ```sh curl "$APP_URL/api/graphql" \ -H 'Content-Type: application/json' \ --data '{"query":"query People { people { id firstName lastName email } }"}' ``` ## Extend the contract [#extend-the-contract] The schema is split into one module per domain (reservations alone span catalog, sessions, waitlists, attendance, chats, staff availability, external booking and bookings). Each module declares its own root fields with `extend type Query` and `extend type Mutation`; `roots.ts` keeps shared enums and the bug report. `backend/src/graphql/schema/index.ts` composes them for Yoga; code generation reads the same module files. The public contract is unchanged by this organization. 1. Edit the relevant module in `backend/src/graphql/schema/` and `web/src/graphql/operations.graphql`. 2. Run `make codegen` to generate resolver and operation types. 3. Update the resolver and native Drizzle query. 4. Update the frontend and tests, then run `make check` and `make test`. Do not edit generated files by hand. CI checks for code generation drift. ## Web GraphQL debugger [#web-graphql-debugger] The **Developer** button at the bottom of the back-office navigation opens a popover with **Doc** and **GraphQL debugger** actions. The debugger opens in a fixed, non-modal bottom panel by default. The rest of the app stays clickable, so requests appear live while you navigate. **Minimize** collapses it to a small GraphQL launcher; click it to reopen. Recording continues while minimized. **Expand** switches to the fullscreen view; **Restore** returns to the panel without resetting the selected request or recording state. Each new opening starts in the bottom panel. It records actual web requests, not cache reads, and displays syntax-highlighted queries, variables, and formatted responses with HTTP status, duration, and failure or cancellation state. It does not execute or replay operations. Recording starts automatically in development, and when the debugger is opened in a production build. There are no pause or clear controls; reload to reset history. Only the latest 50 requests are retained in memory. Variables and response displays are capped at 64,000 characters. Values are not redacted: passwords, tokens, and personal data may be visible. Review debugger output before sharing it. Headers are never recorded and nothing is persisted or sent to a telemetry service. > Person information is restricted to its owner and authorized club administrators. Applicant access > uses a scoped token or authenticated session. See the authentication reference for public > operations, session revocation and password recovery. See [Onboarding operations](/en/graphql/onboarding) for ordered question and special blocks, signature document fields, and save validation. Application review follows explicit status transitions. The club-scoped [`applicationHistory` query](/en/graphql/applications#status-transitions-and-history) returns recorded transitions with server dates, origins and historical rejection reasons. ## Club credits [#club-credits] See [Club credits](/en/graphql/credits) for credit packs, balances, refill history, credit-priced add-ons and reservations, and cancellation refunds. Membership subscriptions stay billed in money. ## Operation limits [#operation-limits] Every GraphQL document is checked before resolver execution, including public operations. The server accepts at most 65,536 query characters and 10,000 parser tokens. After expanding fragment spreads, limits are 20 root fields, 50 aliases, 1,000 fields, 15 field levels and 2,000 selection visits. Fragment cycles cannot bypass these limits. All operations in a document contribute to the budget, even when only one operation is selected; conditional directives do not reduce it. A static cost budget of 100,000 also applies: each field costs its inherited weight, and each list layer multiplies descendant weight by 10. This is a conservative structural estimate, not a measurement of SQL work or returned rows. Existing pagination and business limits still apply. Excessive documents receive `OPERATION_LIMIT_EXCEEDED` without invoking resolvers; parser token limits produce a GraphQL syntax error. HTTP batching remains disabled. ## Error codes [#error-codes] Expected errors are returned unmasked with `errors[].extensions.code`: | Code | Meaning | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BAD_USER_INPUT` | Rejected input or a business refusal; the message can be shown as is. Malformed identifiers return `Invalid ID.` | | `UNAUTHENTICATED` | The caller is not signed in, not as the member or staff account the field requires, or a signed-in staff account lacks the permission or club access the operation needs. Anonymous callers read "You do not have access to this information. Please sign in."; signed-in staff read "Your access does not include this action." | | `FORBIDDEN` | Only for staff professional documents a signed-in staff account may not open, and for the floor plan artwork debugger. | | `NOT_FOUND` | The requested club setting does not exist. | | `CHECKOUT_EXPIRED`, `INSUFFICIENT_CREDITS` | Payment outcomes the apps handle explicitly. | | `SERVICE_UNAVAILABLE` | A service this deployment does not configure (for example sandbox billing, webhooks or floor plans), or a provider that could not be reached. The message names the service. | `PROPOSAL_CHANGED` and `PROPOSAL_UNAVAILABLE` are not `extensions.code` values: they are `ReservationResult.errorCode` values returned in `data` by the reservation proposal mutations (see [reservations](/en/graphql/reservations)). Invalid pages for paginated history fields return `BAD_USER_INPUT` with `Choose a valid page.`. Any other failure is masked as described below. ## Request diagnostics [#request-diagnostics] GraphQL responses include a server-generated `X-Request-Id` header. Unexpected errors keep the generic `Unexpected error.` message and include the same ID in `errors[].extensions.requestId`. Supply this ID when reporting an incident; an HTTP 200 response can still contain a GraphQL error. Incoming request IDs are not trusted or reused. The backend emits a correlated Effect error log for unexpected failures, including failures during authentication-context construction. Logs identify the operation kind and, where available, the schema's root field. Client-supplied operation names, aliases, variables, headers and request bodies are never copied into these logs. Expected validation, authentication and business errors do not produce unexpected-error logs. Console cause diagnostics contain recognized error categories, allowlisted technical codes, stack fingerprints and bounded redacted messages/frames. Exported traces add original exception messages, stacks and error-owned properties, including nested provider causes. These sensitive details are never returned in API responses. Neither path can recover causes already discarded by a provider adapter. ## Trace Correlation [#trace-correlation] When an Effect OTLP endpoint is configured, unexpected masked errors also include `errors[].extensions.traceId` and `spanId`: the actual GraphQL server span's identifiers, shared with its sanitized server diagnostic and nested Effect service spans. These are correlation metadata, never raw messages or stacks. IDs are absent when request tracing is not configured. Production/staging Terraform can enable authenticated Cockpit tracing; local development uses Waggle. Client trace headers are not trusted or used as parents. Error reports in both frontends prefer the configured local Waggle `/traces/?span=` link for valid trace context, otherwise retaining request-ID log search. Production reports retain the access-controlled Cockpit logs dashboard rather than inventing a trace link. Background floor-plan imports run in a separate `floorPlan.import` trace, linked to the initiating GraphQL span with a `follows-from` span link. Its attributes include the import ID and initiating request/trace/span IDs. The job continues after the start response ends; its failure does not retroactively fail that response. Console import reports remain sanitized, while the failed trace retains the original error cause. Client artwork-debugger `traceId` values remain separate application identifiers, not OpenTelemetry trace IDs. # Invoices Browse sandbox subscription invoices, payments, PDF documents and full credit notes. Source: https://docs.champagne.dev-ukab.fr/en/graphql/invoices Stripe technical failures remain masked as `INTERNAL_SERVER_ERROR` and include a server-generated `extensions.requestId` for the sanitized server diagnostic. The original cause is retained only on the server. When Stripe reports an invoice as missing (`resource_missing`), invoice retrieval during refresh or credit-note creation retains its existing `null` result; it is not reported as a provider outage. Finance opens the invoice list at `/sell`; `/sell/invoices/:invoiceId` opens a detail. The selected club, search, status, creation date range and page are URL parameters. Detail pages provide a named back link above their content, opposite the page actions, preserving the previous list filters. Breadcrumbs appear only for paths with at least two levels. Opening a person from an invoice retains the path Invoices / invoice number / People / person name and a back link to that invoice. Section lists have no breadcrumb or back link. The interface retains loaded invoices if a background refresh fails and offers an explicit retry. This increment reads a **PostgreSQL projection of Stripe test data**, using the existing billing configuration. List and detail queries never contact Stripe. It does not create invoices, change numbering or recalculate taxes. Stripe remains authoritative for snapshots, numbering, payments and PDFs. Reading never creates a payment or credit note. ## Scope [#scope] Only EUR sandbox invoices whose subscription snapshot and current subscription contain the current `champagne_iena_memberships_2026_v1` catalog marker or the retired `champagne_basic_membership_monthly_v1` marker are included. Retaining the retired marker keeps historical invoices readable after catalog cleanup. The current subscription's `personId` identifies the Champagne profile; historical invoice metadata may contain an older identity. The required `clubId` restricts the invoice's historical club, even when several clubs share one merchant account. All Stripe reads use the connected account. Memberships match that profile and the invoice line's Stripe Price ID (`stripePriceId`), according to the plan-based membership model. A shared Price never identifies a person. A missing membership does not hide an unpaid invoice. Payer details remain the immutable invoice snapshot and can differ from the current profile. This does not import arbitrary Stripe customers, standalone invoices or Peoplevine invoices. Manual invoice creation, broader plan catalogs, payer reassignment, accounting exports and PCA/CA are separate increments. French invoicing compliance remains a separate concern. Invoice operations require an authenticated administrator of the selected club. Live keys and live invoices are not supported. ## List [#list] ```graphql query Invoices( $clubId: ID! = "51000000-0000-4000-8000-000000000051" $page: Int! = 1 $sort: InvoiceSortField $direction: SortDirection $search: String $status: InvoiceStatus $from: String $to: String ) { invoices( clubId: $clubId page: $page search: $search status: $status from: $from to: $to sort: $sort direction: $direction ) { configured total page pageSize sync { state processed pending failed unmatched lastSyncedAt lastError } items { id number status createdAt dueAt currency total amountPaid amountRemaining credited personId memberName clubId clubName payerName payerEmail } } } ``` The query runs unchanged with empty variables (`{}`), loading page 1 for the seeded Ukab Clubs club. To select another club, enter overrides in the runner's Variables editor (a separate JSON block is not preloaded): ```json { "clubId": "51000000-0000-4000-8000-000000000051", "page": 1 } ``` | Argument | Behavior | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clubId` | Required club UUID; invoices must belong to that historical club. The UI defaults to Ukab Clubs. | | `page` | Required by the example, defaults to 1 in the API; 1–1,000,000. Fixed 10 rows per page. Out-of-range pages clamp to the last page. | | `sort` | `InvoiceSortField`: `number`, `client` (payer name), `issued` (displayed creation timestamp, default), `due`, `total` (minor units), or `status`. Applied before pagination. | | `direction` | `SortDirection`: `asc` or `desc` (default). Each order uses ID in the same direction to break ties. | | `search` | Optional, up to 200 characters. Trimmed case- and accent-insensitive substring match on invoice number, payer name/email or associated person's name. | | `status` | Optional `draft`, `open`, `paid`, `overdue`, `void` or `uncollectible`. `open` excludes overdue invoices. | | `from`, `to` | Optional real calendar dates in `YYYY-MM-DD`. Inclusive UTC creation dates; start must not exceed end. These filter Stripe creation time, which may precede finalization. | An invoice is overdue only when Stripe marks it open, its remaining amount is positive, and its explicit due timestamp is before the server clock. Failed automatic collection without a due timestamp remains open. Credit notes are displayed separately from payment status: a paid invoice can also be fully credited. By default, results are ordered by finalization timestamp (creation timestamp for drafts), newest first, then descending ID. Nullable numbers and due dates follow database null ordering. Text sorting for number, client, and status ignores case and accents; equivalent values use ID as a tie-breaker. `createdAt` exposes that displayed timestamp. `total` counts all scoped/filter-matching rows before pagination. Every amount is an integer in euro cents; the response currency is `eur`. `number` and `dueAt` can be null for drafts or automatically collected invoices. `amountPaid`, `amountRemaining` and `credited` are provider values, not inferred from the total. `credited` includes pre- and post-payment credit notes. Credit notes do not erase the original invoice total. When Stripe is disabled, the query returns `configured: false`, empty items, zero total, page 1, page size 10 and `sync: null`. With configuration, queries return the currently imported rows, including during a Stripe outage. Database failures remain masked errors. During initial import, `total` is the matching **local** count, not a promised final count. ### Synchronization contract [#synchronization-contract] `sync` is nullable for disabled billing and older injected service implementations. The configured runtime returns account synchronization state, not scoped by list filters: | Field | Meaning | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `state` | `importing` before the first scan completes; `syncing` while queued work remains; `ready` when caught up; `error` when any job is awaiting a retry after failure. | | `processed` | Number of distinct invoice IDs with a persisted projection, including unmatched/deleted IDs. Not a percentage or the number of matched invoices. | | `pending` | Unfinished jobs, including the initial scan. Duplicate events have one durable job. | | `failed` | Jobs with a recorded error, still scheduled for automatic retry. | | `unmatched` | Projection rows excluded because they are outside the supported scope, have no local owner, or were deleted in Stripe. | | `lastSyncedAt` | Nullable timestamp of the latest successful projection write, not proof that every invoice is current. | | `lastError` | Nullable sanitized diagnostic; never contains Stripe payloads or credentials. | The UI polls the local `invoices` query every 3 seconds while importing/syncing, 10 seconds after sync or request errors, and 30 seconds when idle to pick up webhook changes. Polling pauses in hidden tabs and stops when billing is unconfigured. Finance displays synchronization status, counts, last sync and errors. Cached rows remain visible on errors, and an empty importing page explicitly indicates that results are not final. Refresh reads local data only; worker retries are automatic, not triggered by the button. ### Durable worker [#durable-worker] Backend startup schedules a scan for each distinct selected connected account when billing is configured. `invoice_job` stores the pagination cursor, processed discovery count, attempts, next attempt time, completion time and sanitized error. Each Stripe list page and its child invoice jobs commit atomically. Restart resumes from the committed cursor; there is no 1,000-invoice ceiling. Initial discovery does not block HTTP startup. The process polls PostgreSQL; there is no Redis. Workers claim jobs with transaction row locks and `SKIP LOCKED`. Refresh and credit operations share a transaction-scoped advisory lock per invoice, acquired **before** fetching Stripe. A lost process releases its locks and rolls back unfinished work. A savepoint prevents partial snapshots on refresh errors. Retries use exponential backoff from 2 seconds to 1 hour, indefinitely. Logs contain only operation and attempt count. Job rows retain event IDs for deduplication; no automatic retention/deletion policy is installed. The existing raw-body `/api/webhooks/stripe` endpoint verifies signatures before inserting event jobs. Subscribe to `invoice.*`, `credit_note.*`, and `invoice_payment.*` events at the SDK-compatible Stripe API version on connected accounts. Live events are refused and events require `event.account`; foreign accounts cannot mutate local projections. Duplicate event IDs are ignored; events never overwrite snapshots directly. Instead the worker retrieves current Stripe state, so delayed/out-of-order delivery cannot restore an old event snapshot. The `invoice.paid` membership handler validates connected context and still runs synchronously after invoice enqueueing; its failures still return HTTP 500. An hourly full reconciliation scan also requeues known projections, covering missed events, deleted drafts, unmatched owners that become available, and profile/membership changes. A refresh that first sees a subscription invoice as paid also applies the `invoice.paid` membership update, so a missed webhook cannot leave a paid membership unpaid. Neither path renews a membership from an invoice settled only by credit notes. Associations and profile names reflect the last successful refresh, not immediate local directory edits. Each invoice stores the complete typed detail JSON plus indexed club/date/status columns and searchable text. Overdue status is derived at query time in PostgreSQL, without waiting for a webhook. Count and page reads share a repeatable-read snapshot. This worker requires a running backend process: scale-to-zero pauses work until restart. Transactions intentionally span Stripe requests to guarantee serialization without leases; large invoices occupy a database connection while their pages load. Resource jobs, cursors and locks are qualified by connected account; a shared account is scanned once. Historical club attribution comes from subscription context, not account identity alone. ## Detail [#detail] ```graphql query Invoice($id: ID! = "in_example", $clubId: ID! = "51000000-0000-4000-8000-000000000051") { invoice(id: $id, clubId: $clubId) { id number status createdAt dueAt currency total subtotal totalExcludingTax amountPaid amountRemaining credited personId memberName clubId clubName subscriptionId membership { id stripePriceId planId expirationDate } issuerName payerName payerEmail payerAddress taxIds pdfUrl paymentUrl creditableAmount taxes { label rate amount } lines { id description plan { name amount currency interval } proration quantity amount periodStart periodEnd } payments { id amount status paidAt } creditNotes { id number amount status createdAt memo pdfUrl } } } ``` The example runs with `{}` using the illustrative ID `in_example`, normally returning `null`; no invoice is created or seeded. To load a real detail, run the list query first, then set `id` in Variables to an `items[].id` from its response. Invoice IDs belong to the configured Stripe sandbox and cannot be hard-coded portably. `id` must start with `in_` followed by 1–100 alphanumeric characters. `clubId` is a required UUID. Disabled billing and invoices outside the selected scope return null. An unknown local ID (including not-yet-imported IDs) returns null. A Stripe outage does not break a previously imported detail. Database failures return a masked error. Detail includes all summary fields and: * Payer snapshot, account name, tax IDs, associated subscription and person link. * `membership`: the local membership matching this profile and invoice Price, or null. The UI explicitly distinguishes an associated profile from a recorded membership. Reading invoices never creates memberships; paid memberships are recorded by the signed `invoice.paid` webhook. The worktree manages its own Stripe listener. A missing membership can mean delivery is pending or payment validation failed. * Subtotal before discounts/taxes and provider total excluding tax (nullable if unspecified). Total includes tax and adjustments. Individual line amounts retain Stripe's semantics, including inclusive taxes where applicable; the PDF is the authoritative document. * Tax amounts and the referenced tax rate's display name/percentage, where available. * Invoice lines with quantity and service period; payment records with their status, amount and paid timestamp. `amount` is the amount received once the payment is `paid`, otherwise the amount the payment requests; `paidAt` stays null until it is paid. All credit notes, including voided notes. * `lines[].plan`: the plan behind the line's Stripe Price (name, and the Price's amount in minor units, currency and `month`/`year` interval), or null when the Price was not created by Champagne. `lines[].proration` marks Stripe proration adjustments. Clients compose the line label from these fields in the reader's language; `description` is Stripe's own English text (nullable), kept only as a fallback for lines without a plan. * Stripe's PDF URL and a hosted payment URL only for open invoices with money remaining. Only HTTPS Stripe-domain document/payment URLs are exposed. Links open in a new tab. * Remaining creditable amount for paid/open invoices, based on original total minus existing credits. Provider restrictions such as separate refunds can further reduce the amount Stripe permits to be credited; Stripe remains authoritative. Synchronization follows every page of invoice lines, invoice payments and credit notes (100 per provider request), and resolves all referenced invoice tax rates. A projection is committed only when the complete detail has loaded; errors preserve its previous value. ## Refresh an invoice [#refresh-an-invoice] ```graphql mutation RefreshInvoice($id: ID!, $clubId: ID!) { refreshInvoice(id: $id, clubId: $clubId) { id status credited creditableAmount } } ``` Both arguments are required and validated as for detail. `refreshInvoice` returns nullable `InvoiceDetail`, with the same selectable fields as `invoice`. It verifies historical club ownership and rereads the resource in its connected account, updating the projection. An invoice from another club cannot be refreshed simply because the merchant is shared. Finance synchronizes the visible invoice with this mutation on opening, on tab focus, and every minute while visible. Lists automatically reread local rows. Failures are explicit and the UI retains previously loaded detail. ## Create a full credit note [#create-a-full-credit-note] ```graphql mutation CreditInvoice($id: ID!, $clubId: ID!, $expectedAmount: Int!, $memo: String!) { creditInvoice(id: $id, clubId: $clubId, expectedAmount: $expectedAmount, memo: $memo) { id credited creditableAmount amountRemaining creditNotes { id number amount status createdAt memo pdfUrl } } } ``` This is an explicit **sandbox write**. `clubId` is required. `expectedAmount` is the positive amount shown in the detail. `memo` must contain 3–500 characters after trimming (the raw input also cannot exceed 500) and no null characters. Invalid arguments return `BAD_USER_INPUT`; provider/internal errors are masked. The UI asks for a reason and confirmation, and disables repeated submission while pending. The server reloads the invoice from live Stripe (not the projection) and verifies scope, eligibility and the expected amount under the same per-invoice lock used by the worker. A successful credit reduces the unpaid amount; any post-payment part becomes customer balance credit for future invoices. It does **not** refund a card, cancel a subscription, change membership access, or send an email (`email_type=none`). Stripe calculates the split and records its official credit note and PDF. The response is the refreshed detail, written to the local projection before returning. A refresh job is also queued before the action; periodic reconciliation provides recovery if a process dies around the external write. The operation uses a stable Stripe idempotency key per invoice. A retry after a completed full credit returns the current invoice without another write; concurrent matching calls share Stripe's idempotency protection. If invoice state changes or Stripe rejects the amount, refresh before retrying. An error after credit creation can mean the write succeeded but the refreshed detail failed: inspect credit notes before taking further action. Partial credits, card refunds and voiding credit notes are not exposed in this increment. ## Member outstanding invoices [#member-outstanding-invoices] `myOutstandingInvoices(personId: ID!, clubId: ID!, page: Int! = 1)` requires a member session matching both identifiers. Staff, applicant-only and anonymous sessions are refused. The separate administrator invoice queries retain their existing authorization. ```graphql query MyOutstandingInvoices($personId: ID!, $clubId: ID!, $page: Int! = 1) { myOutstandingInvoices(personId: $personId, clubId: $clubId, page: $page) { configured page hasMore items { id number amountRemaining currency dueAt pdfUrl paymentUrl } } } ``` The query reads synchronized invoices for the club's connected account and the exact member. It includes only open invoices with a positive remaining amount, including overdue invoices and partially paid invoices. It does not require an active membership. Paid, void, draft, uncollectible and fully credited invoices are excluded. The existing EUR sandbox catalog scope applies; this is not a complete accounting balance or an enterprise payer workflow. Pages contain at most 20 items, ordered by creation date then ID descending. `page` accepts 1–1,000,000; `hasMore` indicates a following page. Amounts are minor currency units and dates are ISO timestamps. No customer contact details, tax information or synchronization diagnostics are exposed. Payment and PDF links accept only HTTPS Stripe URLs without embedded credentials. `configured: false` means invoice access is unavailable, not that nothing is owed. The consumer shows this section in My account and automatically refreshes it. It rechecks every 15 seconds while visible and on window focus, preserving loaded rows when a refresh fails. Payment opens Stripe in another tab. Only synchronized invoice state can remove an invoice from this list; opening a link, returning to the app or changing a card does not mark it paid. When balances change, the account is refreshed too. Reading never triggers a charge, invoice creation or credit. Existing signed Stripe webhook processing remains responsible for confirming the paid membership period; synchronization delays remain possible. ## Member invoice history [#member-invoice-history] `myInvoices(personId: ID!, clubId: ID!, page: Int! = 1): MemberInvoicePage!` returns the authenticated member's issued invoices from the local Stripe projection. Only that member's session, person and club are accepted; staff and applicant access is rejected. Page numbers must be between 1 and 1,000,000. ```graphql query MyInvoiceHistory($personId: ID!, $clubId: ID!, $page: Int!) { myInvoices(personId: $personId, clubId: $clubId, page: $page) { configured page hasMore items { id number status createdAt total currency pdfUrl } } } ``` The response contains up to 20 invoices per page, newest emission first, with invoice ID as a stable tie-breaker. It includes open, overdue, paid, void and uncollectible invoices; drafts and unmatched records are excluded before pagination. Paid invoices remain available even without an active membership. `total` is the invoice total in minor currency units, not the remaining balance; `createdAt` is the emission time. PDF links are nullable and restricted to HTTPS Stripe URLs. No payer contact data, internal notes or staff actions are exposed. `configured: false` distinguishes an unavailable merchant from an empty history. Existing catalog and sandbox scope applies; this is not an import of historical PeopleVine invoices. The consumer's **My invoices** section displays number, emission date, total, status and PDF link. The account automatically refreshes both history and outstanding invoices; returning to the window also refreshes history. Changes detected in outstanding balances invalidate the history. Cached rows remain visible if a background refresh fails. **Outstanding invoices** remains the payment entry point. # Merchant profiles Select, create, activate and prepare a connected sandbox merchant for a club. Source: https://docs.champagne.dev-ukab.fr/en/graphql/merchants Finance at `/sell?club=` lists locally registered PostgreSQL profiles already used by a club in the selected club's owner space, not all Stripe accounts or profiles belonging to another space. Each club selects one merchant; clubs in the same space may share it. Replacement is outside this flow. Merchant operations require an authenticated administrator of the selected club. ```graphql query MerchantProfiles($clubId: ID!) { merchantProfiles(clubId: $clubId) { selectedMerchantId profiles { id name email country entityType legalForm environment platformAccountId stripeAccountId configurationStatus paymentsEnabled payoutsEnabled checkedAt catalogReady } } } ``` Required `clubId: ID!` is a local club UUID. `MerchantProfiles!` contains nullable `selectedMerchantId: ID` and `profiles: [MerchantProfile!]!`. No search, filters, sorting or pagination arguments are accepted. Profiles are ordered by name and local ID. A profile that is unused or used only by a club in another owner space is not returned as a suggestion, and `selectMerchant` refuses it like an unknown profile. ## Profile fields [#profile-fields] | Fields | Types | Meaning | | ----------------------------------------------------- | ---------- | ---------------------------------------------------------------------- | | `id` | `ID!` | Local profile ID used for selection; never send an account ID instead. | | `name`, `email`, `country` | `String!` | Merchant identity; country is a two-letter uppercase code. | | `entityType` | `String!` | `company`, `individual`, `non_profit` or `government_entity`. | | `legalForm` | `String` | Optional legal form, e.g. SAS, separate from Stripe entity type. | | `environment`, `platformAccountId`, `stripeAccountId` | `String!` | Sandbox and Stripe account identity. | | `configurationStatus` | `String!` | `pending` (configuration to complete) or `ready`. | | `paymentsEnabled`, `payoutsEnabled` | `Boolean!` | Separate payment and payout capability state. | | `checkedAt` | `String` | Nullable last verification timestamp. | | `catalogReady` | `Boolean!` | Whether the connected three-offer catalogue is prepared. | ## Select and create [#select-and-create] ```graphql mutation SelectMerchant($clubId: ID!, $merchantProfileId: ID!) { selectMerchant(clubId: $clubId, merchantProfileId: $merchantProfileId) { id configurationStatus } } mutation CreateMerchant($input: CreateMerchantInput!) { createMerchant(input: $input) { id name configurationStatus paymentsEnabled payoutsEnabled catalogReady } } ``` Both return `MerchantProfile!`. Selection validates local UUIDs, club/profile existence, matching sandbox/platform and compatible Stripe account configuration. A club with a different selected merchant is rejected. An incomplete profile is selectable to finish activation, and selection by another club does not exclude it. `CreateMerchantInput` requires `clubId: ID!`, `requestId: ID!`, `name: String!`, `email: String!`, `country: String!`, `entityType: String!`; `legalForm: String` is optional. Use valid UUIDs, a nonblank name of at most 200 characters, an email of at most 254 characters with a local part and dotted domain, a two-letter uppercase country code, a supported entity type and legal form of at most 100 characters. Keep the request UUID stable for retries of the exact same input. Creation provisions Accounts v2, saves the local profile and selects it for the club. French platforms require account tokens for merchant identity. This sandbox-only implementation creates a v2 account token with the test secret key, passes country outside the token, and leaves agreement acceptance to Stripe-hosted onboarding. Merchant configuration uses a full dashboard, Stripe fees paid by the merchant and losses handled by Stripe. Live mode is rejected. Errors are returned through GraphQL. The seeded Ukab Clubs sandbox fixture is intentionally different: bootstrap creates it without a dashboard, accepts test terms and identity data, and leaves requirement, fee and loss responsibility with the platform. Runtime accepts that configuration only with the exact versioned fixture metadata; it does not change merchants created by this mutation. ## Activate, verify and prepare [#activate-verify-and-prepare] ```graphql mutation MerchantOnboarding($clubId: ID!) { merchantOnboarding(clubId: $clubId) } mutation RefreshMerchant($clubId: ID!) { refreshMerchant(clubId: $clubId) { id configurationStatus paymentsEnabled payoutsEnabled checkedAt catalogReady } } mutation PrepareMerchantCatalog($clubId: ID!) { prepareMerchantCatalog(clubId: $clubId) { id catalogReady } } ``` Each requires the club UUID and an existing selected merchant on the configured platform. `merchantOnboarding` returns a non-null Stripe-hosted activation URL (`String!`). After returning, Settings automatically synchronizes capabilities. It also synchronizes the selected merchant every minute while visible. `refreshMerchant` and `prepareMerchantCatalog` return `MerchantProfile!`. Preparation can be resumed/retried and verifies the three connected Product/Price mappings shared by clubs selecting this merchant. Checkout requires enabled payments and a valid catalogue; payouts are displayed separately. The UI invalidates merchant, invoice and affected offer caches after these operations. # Newsletters Staff-written member newsletters sent through each club's own Amazon SES tenant, with test sends, delivery statistics, one-click unsubscribe and member opt-out. Source: https://docs.champagne.dev-ukab.fr/en/graphql/newsletters Staff open **Newsletters** in the back office to write a newsletter, send a test, then send it to the club's members. Delivery goes through Amazon SES, separately from transactional mail. Each club sends from its own domain through its own SES tenant, so one club's bounces or complaints never affect another club's reputation. ## Club sending domain [#club-sending-domain] Before a club can send, an administrator opens **Settings → Integrations → Newsletter sending** and enters a sending domain (a subdomain reserved for newsletters, such as `news.example.com`), a sender name and an address name (default `newsletter`). ```graphql query NewsletterSettings($clubId: ID!) { newsletterSettings(clubId: $clubId) { configured restrictedTo testRecipients sender { domain senderName senderEmail status checkedAt records { type name value } } } } mutation ConfigureNewsletterSender($input: NewsletterSenderInput!) { configureNewsletterSender(input: $input) { senderEmail status } } mutation RefreshNewsletterSender($clubId: ID!) { refreshNewsletterSender(clubId: $clubId) { status checkedAt } } ``` `configureNewsletterSender` and `refreshNewsletterSender` require `settings_manage`; `newsletterSettings` requires `newsletters_manage` or `settings_manage`, so the staff who configure the sender in Settings can read its status. `NewsletterSenderInput` requires `clubId`, `domain` and `senderName`; `localPart` defaults to `newsletter`. The domain is trimmed, lowercased and stripped of a trailing dot, and must be a valid host name; the address name must contain only lowercase letters, digits, `.`, `_` and `-`; the sender name is 1–100 characters without `<`, `>`, `"` or line breaks. Configuring creates (or reuses) the SES domain identity, a configuration set reporting delivery events, and the club's tenant, then links them. The result lists the DNS records to add: three DKIM `CNAME` records and a recommended DMARC `TXT` record. `status` is `pending` until SES sees the DKIM records, `verified` once it does, and `failed` when the identity no longer exists in SES. `refreshNewsletterSender` checks again and returns null when no domain is configured. Settings show the records while the domain is not verified, and behind **Show DNS records** once it is. The sender name is the display name recipients see in their inbox (`"Name"
`); the form prefills it with the club name. Saving again replaces the domain and sender. `configured` reports whether SES credentials are set on the platform; `restrictedTo` lists the allowed recipients outside production (null in production). ## Audience [#audience] A newsletter goes to every person of the club who: * has a membership with no expiration date, or one expiring today or later; * has not unsubscribed from newsletters; * is not banned or disabled; * when the newsletter targets tags, carries at least one of them. Tags are the club's [member tags](/en/graphql/person-tags). A newsletter with no tag goes to every subscribed member. Its tags are saved with the draft; tags deleted afterwards are ignored, and sending is refused when every targeted tag was deleted, so a targeted newsletter never widens to the whole club. Addresses are trimmed, lowercased and deduplicated. Outside production (`NODE_ENV` other than `production`), the club audience is ignored: every newsletter and every test goes only to `NEWSLETTER_ALLOWED_RECIPIENTS`, and an empty list sends nothing. ## Staff operations [#staff-operations] ```graphql query Newsletters($clubId: ID!) { newsletters(clubId: $clubId) { id subject status recipientCount sentAt createdAt } } query NewsletterAudience($clubId: ID!, $tagIds: [ID!]!) { newsletterAudience(clubId: $clubId, tagIds: $tagIds) { members recipients } } query Newsletter($clubId: ID!, $id: ID!) { newsletter(clubId: $clubId, id: $id) { id subject previewText body tags { id label color } status audienceCount recipientCount failure sentAt updatedAt stats { sent delivered bounced complained unsubscribed failed } } } mutation CreateNewsletter($input: NewsletterInput!) { createNewsletter(input: $input) { id status } } mutation UpdateNewsletter($id: ID!, $input: NewsletterInput!) { updateNewsletter(id: $id, input: $input) { id updatedAt } } mutation DeleteNewsletter($clubId: ID!, $id: ID!) { deleteNewsletter(clubId: $clubId, id: $id) } mutation SendNewsletterTest($clubId: ID!, $id: ID!, $emails: [String!]!) { sendNewsletterTest(clubId: $clubId, id: $id, emails: $emails) } mutation SendNewsletter($clubId: ID!, $id: ID!) { sendNewsletter(clubId: $clubId, id: $id) { id status } } ``` These operations require the `newsletters_manage` permission in the requested club; club administrators always have it. IDs must be UUIDs. `newsletters` returns the club's 200 most recent newsletters, newest first; it has no pagination, filters or alternative sorting. `newsletter` returns null for a missing newsletter or one from another club. `NewsletterInput` requires `clubId`. `subject`, `previewText` and `body` default to empty strings and `tagIds` to an empty list, so **New newsletter** creates an empty draft and opens it. Values are trimmed; the subject and preview text are at most 200 characters and the body at most 20,000, and NUL characters are rejected. A subject and a body are required to send or test. Tag IDs must be UUIDs, at most 50, duplicates removed; IDs that are not tags of the club are dropped. The body uses a small Markdown-like syntax, which the editor's toolbar inserts: | Syntax | Result | | --------------------------- | ---------------------------------------- | | `# Title`, `## Title` | Large and medium headings | | `**bold**`, `*italic*` | Bold and italic text | | `[text](https://…)` | Link (`http`, `https` and `mailto` only) | | `- item`, `1. item` | Bulleted and numbered lists | | `> text` | Quote | | `---` | Divider | | `![description](https://…)` | Image on its own line, `https` only | | `[[Label](https://…)]` | Call-to-action button on its own line | A blank line separates paragraphs and single line breaks are kept. Anything else, including HTML, is shown as typed: content is always escaped and links with another scheme stay plain text. Each email has an HTML part and a plain-text part where formatting is reduced to readable text. The email adds the club name as a header, and the club address and a bilingual unsubscribe link as a footer. ```graphql query NewsletterPreview($input: NewsletterPreviewInput!) { newsletterPreview(input: $input) } ``` `newsletterPreview` requires `newsletters_manage` and returns the complete HTML email for unsaved `subject`, `previewText` and `body` (each defaulting to empty), with the same validation as drafts. Its unsubscribe link does not unsubscribe anyone. The editor shows it in a sandboxed frame next to the form and refreshes it shortly after each change. | Status | Meaning | | --------- | ------------------------------------------------------------------------- | | `draft` | Editable and deletable. | | `queued` | Waiting for the newsletter worker, which checks every 10 seconds. | | `sending` | The worker is sending one email per recipient at the account's SES rate. | | `sent` | At least one email was accepted. `sentAt` and `recipientCount` are set. | | `failed` | `failure` explains why. It can be edited and sent again, but not deleted. | `newsletterAudience` counts the audience for any tag selection, so the editor can show it before saving: `members` is the number of subscribed members targeted, `recipients` the number of addresses it would actually be sent to (the allowed recipients outside production). The back office shows the tags as a "To" line above the subject, with the member count beneath; its menu also searches, creates and deletes the club's tags. `audienceCount` is only computed for a draft or failed newsletter fetched with `newsletter`; it is null in the list. `stats` is only computed for a newsletter fetched with `newsletter` once sending started: `sent` counts emails accepted by SES, `delivered` those the receiving server accepted, `bounced` permanent bounces, `complained` spam reports, `unsubscribed` recipients who used their link and `failed` emails SES refused. Open and click tracking is not used. `updateNewsletter` returns null for a missing newsletter and a validation error once it has been queued. `deleteNewsletter` only deletes drafts and returns false otherwise. `sendNewsletterTest` sends the current content, with the subject prefixed by `[Test]`, to 1–5 addresses from `newsletterSettings.testRecipients`: the club's team (staff with access to the club and the owner of its space), or `NEWSLETTER_ALLOWED_RECIPIENTS` outside production. Other addresses are refused, so a newsletter permission cannot be used to email strangers from the club's domain. The back office lists these addresses as checkboxes, with the signed-in staff member selected. The test's unsubscribe link does not unsubscribe anyone. `sendNewsletter` queues a draft or failed newsletter with a non-empty body; it returns null for a missing newsletter. Both return a validation error when sending is not configured, when the club's sending domain is not verified, or with SES's explanation when it refuses an email. Every recipient has a delivery record, so an interrupted sending resumes where it stopped: a newsletter left in `sending` for 10 minutes is queued again, only recipients not yet attempted are sent, and an email whose outcome is unknown is not sent twice. A newsletter where SES refused every email becomes `failed` with SES's reason. Sending a `failed` newsletter again discards its previous delivery records and sends to its current audience, with its edited content and tags. ## Unsubscribing and delivery events [#unsubscribing-and-delivery-events] Every email carries a personal link to `/newsletter/unsubscribe?token=…` on the member site, where the recipient confirms. It also carries `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click` headers pointing to `POST /api/newsletters/unsubscribe?token=…`, which mail clients call directly. A `GET` on that URL redirects to the confirmation page without unsubscribing, so link scanners cannot unsubscribe anyone. Unknown tokens answer 404. ```graphql mutation UnsubscribeNewsletter($token: String!) { unsubscribeNewsletter(token: $token) { clubName } } ``` `unsubscribeNewsletter` is public: holding the link is the authorization. It returns null for an unknown token and the club name otherwise; repeating it is harmless. SES publishes delivery, bounce, complaint and rejection events to an SNS topic delivered to an SQS queue that the API reads continuously. Events update the matching delivery record; a permanent bounce or a complaint also unsubscribes that person from the club's newsletters. ## Member subscription [#member-subscription] ```graphql query MyNewsletterSubscription($clubId: ID!) { myNewsletterSubscription(clubId: $clubId) } mutation SetMyNewsletterSubscription($clubId: ID!, $subscribed: Boolean!) { setMyNewsletterSubscription(clubId: $clubId, subscribed: $subscribed) } ``` Members are subscribed by default and can change it under **My account → Notifications → Preferences**. Both operations require member authentication in the club and return the resulting subscription state. Staff cannot use them. ## Configuration [#configuration] | Variable | Purpose | | ------------------------------- | --------------------------------------------------------------------------- | | `SES_ACCESS_KEY_ID` | IAM access key for SES and the events queue. Enables sending. Secret. | | `SES_SECRET_ACCESS_KEY` | Its secret. | | `SES_REGION` | AWS region of the SES account, for example `eu-north-1`. | | `SES_EVENTS_TOPIC_ARN` | SNS topic each club configuration set reports events to. | | `SES_EVENTS_QUEUE_URL` | SQS queue subscribed to that topic. Without it, events are not recorded. | | `CONSUMER_ORIGIN` | Member site origin used for unsubscribe links. Required with SES. | | `NEWSLETTER_ALLOWED_RECIPIENTS` | Comma-separated addresses that receive every newsletter outside production. | # Member notifications Member inbox, per-category channel preferences, manual staff messages, reminders, and session cancellation. Source: https://docs.champagne.dev-ukab.fr/en/graphql/notifications Members open **My account → Notifications** for their inbox and **Preferences** for channel settings. The inbox shows read and unread entries, links to the relevant account page, and supports marking one or all entries read, or one entry unread. **All / Unread** filters the complete inbox before pagination; changing the filter returns to page 1. Page navigation returns focus and scroll to the inbox heading. Pages contain 20 items, newest first (creation time, then ID descending). ```graphql query Notifications($clubId: ID!, $page: Int = 1, $unreadOnly: Boolean = false) { myNotifications(clubId: $clubId, page: $page, unreadOnly: $unreadOnly) { items { id category title detail startsAt timeZone body href createdAt readAt } unreadCount hasMore } myNotificationPreferences(clubId: $clubId) { category inApp email } } mutation Preferences($clubId: ID!, $input: NotificationPreferenceInput!) { setMyNotificationPreference(clubId: $clubId, input: $input) { category inApp email } } mutation Read($clubId: ID!, $id: ID) { markMyNotificationsRead(clubId: $clubId, id: $id) } mutation Unread($clubId: ID!, $id: ID!) { markMyNotificationUnread(clubId: $clubId, id: $id) } ``` All operations require member authentication in the requested club. The recipient comes from the session; the API accepts no recipient ID. Staff cannot use these member operations. IDs must be UUIDs; `page` defaults to 1 and accepts 1–10,000. `unreadOnly` defaults to false (including explicit null); true returns only entries with no `readAt`. Filtering happens before the 20-item page limit and offset. Alternative sorting is not exposed. `readAt` is nullable; timestamps are ISO 8601 UTC strings. `detail` contains a nullable snapshot of the member, reservation or plan context. A reservation's start is not written into `detail`: `startsAt` (ISO 8601 UTC, nullable) carries it with the club's IANA `timeZone`, so the member app formats it in the interface language ("Le Rooftop · 20 oct. 2026, 19:00"); both are null for other notifications. `href` is a relative consumer route. `unreadCount` covers the complete visible inbox, not just the current page. Omitting `id` marks all currently unread entries read. Both read-state mutations return whether any row changed. `markMyNotificationUnread` requires a notification UUID and resets only its `readAt` to null; it returns false for an already-unread, missing, hidden, or other member’s notification. Neither mutation generates notifications or resends email. `markMyCustomerChatRead` also marks read the member's unread `chat` notifications of that conversation (`href` `/account/chats/`), so a club message counts once in the inbox badge. `NotificationPreferenceInput` requires `category`, `inApp`, and `email`. Categories are: | Category | Events | | ------------- | ----------------------------------------------------------------------------------------------------- | | `reservation` | Reservation creation, status and detail changes | | `proposal` | A new or changed staff date proposal | | `sponsorship` | A sponsored person receives a membership (excluding add-ons) | | `guest` | The member's guest accepts an invitation | | `reminder` | A confirmed reservation starts within 24 hours | | `waitlist` | A place becomes available, or a waiting member books the session | | `renewal` | A Stripe subscription's paid period ends within 7 days, or its price changes after one month's notice | | `session` | A booked or waitlisted session occurrence is canceled | | `message` | A professional sends a reservation chat message | | `chat` | A professional starts or answers a customer chat | | `club` | A club administrator sends a manual message from the back office | | `credits` | A credit debit drops the balance below the club's low balance alert | Both channels default to enabled. Saving a category updates only that category. In-app settings apply to future notifications; existing inbox history remains. Email preferences are checked again before a queued email is claimed. Re-enabling a category does not backfill suppressed events. The existing reservation confirmation mail journal respects the `reservation.email` preference; the notification worker does not send another confirmation email. Notification emails write `startsAt` like the reservation confirmation email ("Tuesday 27 October 2026 at 10:00", followed by "Times shown in Europe/Paris."), so a member promoted from the waitlist receives both emails with the same date format. ## Manual staff messages [#manual-staff-messages] Club administrators open a member's profile in the back office and choose **Send notification**. The dialog offers three categories: **Received package** and **Lost object** prefill an editable message, and **Other** requires a custom one. The member receives the message in the `club` category through the usual in-app and email channels. ```graphql mutation Notify($input: SendPersonNotificationInput!) { sendPersonNotification(input: $input) { id category title body href createdAt } } ``` `SendPersonNotificationInput` requires `personId`, `clubId`, and `kind` (`received_package`, `lost_object`, or `other`); `body` is optional. The caller must be an authenticated administrator of `clubId`, and the person must belong to that club. Both IDs must be UUIDs. The mutation returns the created `MemberNotification`, or `null` when the person is not found in the club. | Kind | Title | Default body | | ------------------ | ------------------------ | --------------------------------------------------------------------------------------------------- | | `received_package` | `Package received` | `A package addressed to you has arrived at the club. Please collect it at the reception.` | | `lost_object` | `Lost object found` | `An item that may belong to you was found at the club. Please contact the reception to recover it.` | | `other` | `Message from your club` | None; `body` is required | `body` is trimmed and replaces the default text when present. It is limited to 2,000 characters and must not contain null characters; an empty or missing `body` with `kind: other` returns `BAD_USER_INPUT`. Titles and default bodies are stored in English and translated by the member app; the back office sends `body: null` when the prefilled text is left unchanged so that translation still applies. `href` is always `/account/notifications`. Each send creates a new entry: there is no deduplication of repeated manual messages. The `club` in-app and email preferences apply, and the email is delivered by the notification worker like any other category. ## Scheduling and delivery [#scheduling-and-delivery] Notification records commit in the business transaction independently of club webhook settings. The existing outbound webhook transport remains available and retains its own selection settings. The API's scoped worker scans each minute, at most 100 due records per event category, and processes up to 100 notification emails. Reminder windows are currently fixed at 24 hours and 7 days. Reservations booked inside the reminder window are eligible on the next scan. Renewal reminders use `paidPeriodEnd`, not an inferred anniversary; manual memberships and add-ons are excluded. An approaching period end is not proof of a successful future charge. Unique event keys prevent repeated reminder creation, including when channels are disabled. Rescheduling a reservation or advancing a paid period creates a new reminder key. Before email send, canceled/rescheduled reservation reminders and outdated membership reminders are skipped. Expired reminder emails are also skipped. Proposal emails are sent only while the notified proposal version is still current, awaiting acceptance, and its start time is in the future. Replaced, accepted, canceled, or expired proposals are skipped, including on retry. Inbox entries remain historical records. When a place opens, the waitlist worker books eligible waiting members through the normal reservation checks. Successful booking removes the waitlist entry and emits `waitlist.joined`; the notification links to the resulting reservation and asks the member to check its status and any payment due. A member booking directly while on the waitlist receives the same notification. Notification emails are subject ` · ` and greet the member by first name, followed by the body, the context and a link to the page in the member's club account. Reservation confirmation, security deposit and reservation invitation emails use the same `<club name> · …` subject. A reservation's context names the session title when the reservation belongs to a titled session, otherwise the category. "Reservation update" notifications describe the new status in the member app's words: `pending` is "Your request was sent to the club.", `booked` is "Your reservation is booked, payment is due.", and `confirmed` and `completed` say so. A cancellation adds what happened to the price, read from the reservation when the notification is created: "The price was not refunded." when it was kept past the cancellation notice, "Your payment is being refunded." after a card refund, or "The price returned to your credit balance." after a credit refund; otherwise "Your reservation is canceled.". Email requires the configured mail provider and `CONSUMER_ORIGIN`. The worker commits a sending claim before contacting the provider. Definite failures retry after one hour; uncertain outcomes and abandoned sending claims become `unknown` and are not automatically resent. Provider acceptance is not inbox-delivery confirmation. These are in-app and email channels; browser/mobile push and SMS are not configured. ## Session occurrence cancellation [#session-occurrence-cancellation] ```graphql mutation CancelSession($clubId: ID!, $categoryId: ID!, $occurrenceId: String!) { cancelReservationSession(clubId: $clubId, categoryId: $categoryId, occurrenceId: $occurrenceId) } ``` Club admins and staff assigned to the reservation category can cancel a future occurrence. IDs follow `reservationAvailability`: a custom session ID, or weekly definition ID plus `:YYYY-MM-DD` in club time. A malformed occurrence ID (not one of those two shapes) is rejected with `BAD_USER_INPUT`. Invalid scope or a past occurrence returns false; an already-canceled occurrence returns true. The back-office session roster exposes **Cancel session** with a confirmation step. Cancellation closes that occurrence to bookings/waitlisting, cancels pending/booked/confirmed reservations and notifies their owners, then removes and notifies its waitlist. Other occurrences of a recurring definition remain available. Reservation lifecycle webhooks still fire, but members receive the session-category notification rather than a second reservation-category cancellation. Paid reservations are refunded by a scoped worker using the existing idempotent Stripe refund operation; failures remain discoverable and retry each minute. Session cancellation is effective while refunds are pending. Deleting a session definition is rejected while active reservations, waitlist entries, or unrefunded payments remain; cancel the affected occurrences first. ### Undo marking all notifications read [#undo-marking-all-notifications-read] ```graphql mutation ReadAll($clubId: ID!) { markAllMyNotificationsRead(clubId: $clubId) { ids readAt } } mutation UndoReadAll($clubId: ID!, $receipt: NotificationReadReceiptInput!) { undoMyNotificationsRead(clubId: $clubId, receipt: $receipt) } ``` `markAllMyNotificationsRead` atomically marks all currently unread in-app notifications for the authenticated member and club, across all pages. It returns the exact changed UUIDs and their UTC ISO timestamp (`readAt`); `ids` is empty when nothing changed. Pass that receipt to `undoMyNotificationsRead` to restore those notifications to unread. Only records still carrying the receipt's timestamp are restored, preserving later read-state changes. The mutation returns the restored UUIDs and is safe to repeat. IDs must be valid UUIDs and `readAt` must be a UTC ISO timestamp with milliseconds; otherwise the mutation fails with `BAD_USER_INPUT`. Both operations require member authentication for the selected club and leave previously read entries, other members, notification delivery and email unchanged. # 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. # People selection Search and filter people, understand sorting and pagination, and retrieve person details. Source: https://docs.champagne.dev-ukab.fr/en/graphql/people Use `peoplePage` to select people from the directory. Filters are applied before counting and pagination, and all supplied filters must match. The endpoint is `/api/graphql`; see the [API overview](/en/graphql) for transport and development tooling. ## Edit personal information [#edit-personal-information] The back-office person profile provides **Edit information**, **Save changes**, and **Cancel**. Cancel discards the draft without sending a mutation. A successful save updates cached profiles and invalidates people lists so names, search, age/gender filters, totals, and sorting reflect the saved values. Failed saves keep the draft for correction and retry. Members can edit their own details from the consumer account (see [Members edit their own profile](#members-edit-their-own-profile)). Send `updatePerson(input: UpdatePersonInput!): PersonResult!` by POST: ```graphql mutation EditPerson($input: UpdatePersonInput!) { updatePerson(input: $input) { person { id firstName lastName email type siret tva phoneNumber birthDate gender address { line1 line2 city postalCode state country } } error } } ``` Example variables (use an existing person and their actual club ID): ```json { "input": { "id": "00000000-0000-4000-8000-000000000001", "clubId": "51000000-0000-4000-8000-000000000051", "firstName": "Alex", "lastName": "Morgan", "email": "alex@example.com", "phoneNumber": "+33 1 23 45 67 89", "birthDate": "1990-06-15", "gender": null, "address": { "line1": "12 rue de la Démonstration", "line2": null, "city": "Reims", "postalCode": "51100", "state": null, "country": "France" } } } ``` | Input | Validation | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id`, `clubId` | Required UUIDs; the update matches both IDs atomically. | | `firstName`, `lastName` | Required strings, 1-100 characters after trimming. | | `email` | Required, trimmed and lowercased, at most 254 characters; must have a non-whitespace local part and domain with a dot. The same email can belong to different clubs; duplicates within one club are rejected when creating or editing a person, including case-only differences. | | `phoneNumber` | Optional string, at most 50 characters after trimming. | | `birthDate` | Optional real calendar date in `YYYY-MM-DD`, year 0001 or later, no later than today in UTC. | | `gender` | Optional `female`, `male`, `nonbinary`, or `other`; blank, omitted, or null clears it. | | `address` | Optional `AddressInput` object (`line1`, `line2`, `city`, `postalCode`, `state`, `country`). `line1`, `city`, `postalCode`, and `country` are required together; blank, omitted, or null clears it. Fields are Unicode-normalized (NFKC) with collapsed whitespace; limits: `line1`/`line2` 200, `city`/`state`/`country` 100, `postalCode` 20 characters. | The input also accepts optional legal fields: | Input | Validation | | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `individual` or `entreprise`; omitted or null preserves the stored type. | | `siret` | Omitted preserves it. Supplied values are trimmed and must contain exactly 14 digits. Blank or null clears it only for an individual. An enterprise requires a valid supplied or stored SIRET. | | `tva` | VAT identification number, at most 32 characters after trimming. Omitted preserves it; blank or null clears it. | This is a replacement of these personal fields, not a partial patch: omitted, null, or blank optional fields are cleared to null. Avatar, club assignment, memberships, and additional fields are not changed. On success `person` contains the complete `PersonDetail` (the same selectable fields as the person query) and `error` is null. A missing person or wrong club returns `person: null` with an unavailable error. An email conflict returns `person: null` with a correction message, without changing the record. Invalid input produces a GraphQL `BAD_USER_INPUT` error before writing; unexpected failures are masked. Concurrent valid saves use last-write-wins semantics. Person management is staff-only and scoped to the administrator’s clubs (see [Authentication](/en/graphql/auth)). ### Edit legal status [#edit-legal-status] The **Legal status** card above **Memberships** offers **Edit legal status** to staff with `members_manage`. Select **Individual** or **Company**; a company requires a 14-digit SIRET and may have a TVA number. Save updates the profile and refreshes the people list, including its type filter. Cancel discards the draft without writing. Switching to Individual hides and preserves identifiers so switching back restores them. Use the same `updatePerson` mutation and include the current personal fields with, for example, `"type": "entreprise", "siret": "01234567890123", "tva": "FR12012345678"`. Legal fields are partial updates, unlike the personal fields. The database checks the effective type and SIRET under a row lock, so omitting a SIRET cannot bypass the requirement for an enterprise. A missing effective SIRET returns `person: null` and `error: "SIRET must contain exactly 14 digits."` without changing any fields. Malformed supplied identifiers return `BAD_USER_INPUT` before writing. Photo-only updates and member self-service preserve legal status without requiring these fields. `PersonDetail.application` is the nullable summary of the application that created the person. It is populated only when a submitted application is accepted; people created by other flows return null. The back office links this summary to the full application review. ## Members edit their own profile [#members-edit-their-own-profile] A signed-in member edits their own details with `updateMyProfile(input: UpdateMyProfileInput!): PersonResult!`. The input contains `clubId` (the member's session club) plus `firstName`, `lastName`, `phoneNumber`, `birthDate`, `gender` and `address`, validated exactly like `updatePerson`. The person ID always comes from the session. Email, photo, status and memberships stay managed by staff. Staff sessions and other clubs are refused with `UNAUTHENTICATED`. Changes are recorded in the activity log as `person.updated`. ```graphql mutation UpdateMyProfile($input: UpdateMyProfileInput!) { updateMyProfile(input: $input) { person { id firstName lastName phoneNumber } error } } ``` ## Manage member status [#manage-member-status] Every person starts with `status: active`. The back-office profile can suspend a member between required start and end dates, block a member's bookings (`no_booking`) between required start and end dates, ban a member indefinitely with a required reason, or disable a member. Suspensions and booking blocks may include an optional reason. Suspensions, booking blocks and bans can be ended early; disabled members can be re-enabled. These actions return the member to `active` and clear the stored reason and dates. Billing state belongs to each membership and never changes a person's access status. A `no_booking` member keeps club access (building entry, credits, add-ons, guests and sponsorship) but cannot create reservations or join waitlists: member reservation requests return `"Your bookings are currently blocked."`, and waitlisted entries are not promoted. Staff can still book for them with `createStaffReservation`; the back-office booking form shows a warning for any member who cannot book for themselves. Clubs can also suspend, block bookings or ban members automatically after repeated no-shows; see [No-show sanctions](/en/graphql/reservations#no-show-sanctions). Send `updatePersonStatus(input: UpdatePersonStatusInput!): PersonResult!` by POST: ```graphql mutation SuspendMember($input: UpdatePersonStatusInput!) { updatePersonStatus(input: $input) { person { id status statusReason statusStartsAt statusEndsAt } error } } ``` ```json { "input": { "id": "00000000-0000-4000-8000-000000000001", "clubId": "51000000-0000-4000-8000-000000000051", "status": "suspended", "reason": "Repeated no-shows", "startsAt": "2026-10-01", "endsAt": "2026-10-15" } } ``` `id` and `clubId` are required UUIDs and both must match the stored person. For `suspended` and `no_booking`, `startsAt` and `endsAt` are required real `YYYY-MM-DD` dates. Neither can be before the current UTC date, and the end date cannot precede the start date. The back office defaults `startsAt` to today. `reason` is optional and limited to 2,000 characters. Before a future start date and after the end date, the effective status is `active`. For `banned`, a non-blank reason is required and both dates must be omitted. For `active` and `disabled`, the reason and both dates must be omitted or null. Unexpected failures are masked, and invalid combinations return `BAD_USER_INPUT`. Membership records expose `clubId` (nullable for an unscoped membership). Member clients must match it to their club or accept null, require a subscription plan (or no plan), and compare `expirationDate` inclusively with the current UTC date before offering a new reservation. An existing Stripe price does not prove current membership validity. ## Manage membership plans [#manage-membership-plans] The back-office profile's **Memberships** card lists every membership with its plan name, subscription/add-on type, price, expiration date, and payment status, plus a **Manage plans** action for full control. Plan catalog data loads only when the dialog opens. Paying for a plan is deliberately not part of this flow: members subscribe and pay through the consumer app, which owns offer selection, Stripe Checkout, and webhook confirmation (see [Sandbox membership billing](/en/graphql/billing)). The back office edits the membership record itself: complimentary grants, corrections, and removals. Send `assignMembership(input: AssignMembershipInput!): PersonResult!` by POST to grant a plan (complimentary access, correction, or onboarding fix): ```graphql mutation GrantMembership($input: AssignMembershipInput!) { assignMembership(input: $input) { person { id memberships { id plan { name type } expirationDate } } error } } ``` ```json { "input": { "personId": "00000000-0000-4000-8000-000000000001", "clubId": "51000000-0000-4000-8000-000000000051", "planId": "52000000-0000-4000-8000-000000000052", "expirationDate": "2027-09-30" } } ``` | Input | Validation | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `personId`, `clubId`, `planId` | Required UUIDs; the person must belong to the club and the plan must be active and offered by the club's merchant catalogue, otherwise `error` is `This plan is not offered by this club.` | | `expirationDate` | Optional real calendar date in `YYYY-MM-DD`; omit or pass `null` for no expiry (typical for add-ons). | The grant creates a manual membership with no Stripe linkage (`stripePriceId: null`), or updates the expiry when the same person already holds that plan manually. It never touches Stripe subscriptions and never collects payment: members who must pay do so through the consumer app. Send `revokeMembership(input: RevokeMembershipInput!): PersonResult!` by POST to remove a membership after confirming in the dialog: ```graphql mutation RevokeMembership($input: RevokeMembershipInput!) { revokeMembership(input: $input) { person { id memberships { id } } error } } ``` `membershipId`, `personId`, and `clubId` are required UUIDs; the membership must belong to the person, and a club-scoped membership must match the club. Removing a Stripe-paid subscription row does not cancel the Stripe subscription itself, so a later `invoice.paid` webhook may project it again; removing a manual membership is final. Both mutations return the complete `PersonDetail` and refresh cached profiles and people lists like other person edits. A missing person, wrong club, or unknown plan/membership returns `person: null` with an unavailable error; malformed IDs or dates produce `BAD_USER_INPUT`. ## Select people [#select-people] Edit the example with the pencil, expand **Variables** to enter a JSON object, then select **Run query**. Without variables, this query returns the first ten people across all clubs and membership states. ```graphql query SelectPeople( $sort: PeopleSortField $direction: SortDirection $page: Int! = 1 $pageSize: Int! = 10 $search: String $clubId: ID $minAge: Int $isMember: Boolean $maxAge: Int $gender: PeopleGender $type: PersonType $status: PersonStatus $attributes: [PersonAttributeFilter!] ) { peoplePage( sort: $sort direction: $direction page: $page pageSize: $pageSize search: $search clubId: $clubId minAge: $minAge isMember: $isMember maxAge: $maxAge gender: $gender type: $type status: $status attributes: $attributes ) { items { id firstName lastName email status } total page pageSize } } ``` For example, these variables select up to 25 people whose searchable text contains `alex`: ```json { "page": 1, "pageSize": 25, "search": "alex" } ``` ### Arguments [#arguments] | Argument | Type | Default | Behavior | | ---------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------ | | `page` | `Int!` | `1` | One-based page number, from 1 through 1,000,000. | | `pageSize` | `Int!` | `10` | Must be 10, 25, or 50. | | `search` | `String` | None | Case- and accent-insensitive substring search over first name, last name, and email. Maximum 200 characters. | | `clubId` | `ID` | None | Restricts results to people whose club ID equals this UUID. | `sort: PeopleSortField` accepts `name` (default), `email`, or `status`. `direction: SortDirection` accepts `asc` (default) or `desc`. Omit either argument or pass `null` to use its default. Unsupported enum values are rejected by GraphQL. Sorting applies to all matching people before pagination. Omit an optional filter or pass `null` to disable it. Omit `page` and `pageSize` to use their defaults; explicitly passing `null` is invalid for these non-null arguments. | Argument | Type | Default | Behavior | | -------- | -------------- | ------- | ------------------------------------------------------------------------------------- | | `minAge` | `Int` | None | Inclusive minimum completed years, nonnegative. | | `maxAge` | `Int` | None | Inclusive maximum completed years, at least `minAge` when both are supplied. | | `gender` | `PeopleGender` | None | Exact stored value: `female`, `male`, `nonbinary`, `other`, or `unknown` (null only). | | `status` | `PersonStatus` | None | Effective status: `active`, `suspended`, `no_booking`, `banned`, `disabled`. | `attributes: [PersonAttributeFilter!]` filters on the club's typed member attributes; see [Member attributes](/en/graphql/person-attributes#filter-people). `tagIds: [ID!]` keeps people carrying at least one of the club's tags and `untagged: true` keeps only people without any tag; see [Member tags](/en/graphql/person-tags#read-tags-on-people). All three require `clubId`. ### Person type [#person-type] The optional `type: PersonType` filter accepts `individual` or `entreprise`. Omit it or pass `null` for both types; unsupported values are rejected by GraphQL. For example, `{"type": "entreprise"}` selects enterprises. It combines with all other filters before counting and pagination, without changing sort order. Existing records and new rows without an explicit type default to `individual`. Both `Person` and `PersonDetail` expose a non-null `type: PersonType!` field. Both also expose `activated: Boolean!`, true once the person has a password (see [account activation](/en/graphql/auth#activate-a-staff-created-account)). `PersonDetail` also exposes nullable `siret: String` and `tva: String` identifiers. These fields are text, not numeric values; leading zeroes are preserved. The back-office Legal status card displays them only for `entreprise` records, showing "Not specified" for missing values. Editing an enterprise requires a 14-digit SIRET; TVA stays optional and has no country-specific format validation. ### Age and gender [#age-and-gender] Ages use today's UTC date. Either age bound excludes missing or future birthdates. Bounds may be supplied independently; zero and equal bounds are valid. Negative or reversed bounds produce `BAD_USER_INPUT`; GraphQL rejects fractional values, strings, values outside its 32-bit signed integer range, and unsupported genders. For example, variables `{"minAge": 18, "maxAge": 65, "gender": "female"}` select women aged 18 through 65. These filters combine with club and text search before counting and pagination. ### Membership links [#membership-links] The optional `isMember: Boolean` argument defaults to no membership restriction. Pass `true` to select people with at least one linked membership, or `false` to select people with none. Omit it or pass `null` to include both groups. For example, `{"isMember": true, "minAge": 18}` selects adults with a linked membership. Expired memberships and memberships without an expiration date or Stripe Price ID still count: this checks record existence, not active or paid status. Multiple linked memberships never duplicate a person or inflate the total. This filter combines with the other filters before counting and pagination. ### Account status [#account-status] The optional `status: PersonStatus` filter selects a person's effective account status. An elapsed suspension is returned and filtered as `active`, even if the stored suspension has not yet been cleared. A future suspension is also active until its start date. Payment state does not affect this filter. Status combines with every other filter before counting and pagination. ## Filters [#filters] ### Text search [#text-search] The API trims leading and trailing whitespace, then searches the concatenation of first name, last name, and email, separated by spaces. Matching is case- and accent-insensitive (`lea` finds `Léa`, `LÉA` finds `Lea`; `œ`, `æ`, `ß` and `ø` match `oe`, `ae`, `ss` and `o`, as in name sorting) and does not require a prefix match. An empty or whitespace-only search applies no text filter. The text is one substring, not a set of independently matched words. For example, `Alex Morgan` can match a full name. `%`, `_`, and backslash are escaped and treated as literal characters, not SQL wildcard syntax. The 200-character limit is checked before trimming. ### Club [#club] Pass a club UUID from `clubDirectory` as `clubId`. Without this filter, `peoplePage` uses the administrator’s first authorized club. A valid but nonexistent club UUID returns an empty selection; a malformed UUID produces a validation error. Every person operation enforces ownership or administrator club access (see [Authentication](/en/graphql/auth)). ## Sort order [#sort-order] By default, results use ascending database order by: 1. `firstName` 2. `lastName` 3. `id`, to break ties between identical names Set `sort` to `email` or `status` to sort by that column instead, and set `direction` to `desc` for descending order. Name sorting uses first name then last name in the requested direction; every order ends with ID as a stable tie-breaker. Text comparisons ignore case, accents, and equivalent composed Unicode characters; equivalent text is resolved by ID. The unpaginated `people` query retains its fixed ascending name order. ## Pagination and return fields [#pagination-and-return-fields] `peoplePage` returns a non-null `PeoplePage` object: | Field | Type | Meaning | | ---------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `items` | `[Person!]!` | The current page; each person has non-null `id`, `type`, `firstName`, `lastName`, `email`, and effective `status`, plus nullable `guestExpiresAt` and `externalAccount` (true for temporary accounts from [external reservations](/en/graphql/external-reservations)), and non-null `activated` (false while a staff-created account awaits [activation](/en/graphql/auth#activate-a-staff-created-account)). | | `total` | `Int!` | Number of people matching all filters, before pagination. | | `page` | `Int!` | The actual page returned, which can be lower than the requested page. | | `pageSize` | `Int!` | The requested, validated page size. | The last page is `max(1, ceil(total / pageSize))`. A valid request beyond the last page is clamped to that page. If nothing matches, the response has `items: []`, `total: 0`, and `page: 1`. The final page can contain fewer than `pageSize` items. Use the returned `page` to update pagination controls. Reset the requested page to 1 when changing filters or page size to start at the beginning of the new selection. The count and items are read in one read-only, repeatable-read transaction, so they share a consistent database snapshot within a request. Pagination uses offsets, not cursors; changes between requests can move people between pages. ## Bounded people list [#bounded-people-list] `people { id firstName lastName email status }` requires staff authentication and returns at most 100 people from clubs where the caller has `members_read`. The club scope is applied before ordering by first name, last name and ID, and before the 100-row limit. People without a club are excluded. This query has no pagination metadata; use `peoplePage` for a paginated list within one club. ## Find clubs for the filter [#find-clubs-for-the-filter] ```graphql query FindClubs($search: String, $selectedId: ID) { clubDirectory(search: $search, selectedId: $selectedId) { items { id name address { line1 line2 city postalCode state country } } selected { id name } hasMore } } ``` * `search` optionally matches a case-insensitive substring across the club name and the address `line1`, `city` and `postalCode` fields, with the same trimming, literal wildcard handling, and 200-character limit as people search. `line2`, `state` and `country` are not searched. * `address` is a nullable `Address` object, the same type as person and application addresses. * Anonymous and member callers use the public club directory. Staff callers see only their authorized clubs; this scope is applied before the limit and also to `selected`. * `items` contains at most 100 clubs, ordered by name and then ID ascending. * `hasMore` is `true` when more than 100 clubs match within that scope. Narrow the search; there is no page or cursor argument. * `selectedId` optionally looks up one club independently of the search and 100-item limit, so a selector can retain its current label. * `selected` is `null` when no ID is supplied or the club does not exist or is outside the staff caller’s scope. A malformed `selectedId` UUID is rejected. ## Update a club address [#update-a-club-address] ```graphql mutation UpdateClubAddress($clubId: ID!, $address: AddressInput) { updateClubAddress(clubId: $clubId, address: $address) { id address { line1 line2 city postalCode state country } } } ``` * `updateClubAddress(clubId: ID!, address: AddressInput): Club` requires the `settings_manage` permission in the requested club; other callers receive `UNAUTHENTICATED`. * `address` uses the same `AddressInput` and validation as person addresses: values are trimmed and whitespace-collapsed, blank fields become null, and `line1`, `city`, `postalCode` and `country` are required once any field is filled. Limits are 200 characters for `line1` and `line2`, 100 for `city`, `state` and `country`, and 20 for `postalCode`. Invalid input returns `BAD_USER_INPUT`. * Passing `null` or an all-blank address clears it. * The mutation returns the updated club. A malformed `clubId` or a club that does not exist returns `BAD_USER_INPUT`. Changes are recorded in the activity log as `club.address_updated` with the formatted before and after addresses. ## Retrieve a selected person's details [#retrieve-a-selected-persons-details] Pass an ID from `peoplePage.items` to `person`. Replace the example ID with a person from your selection when using a different dataset. ```graphql query SelectedPerson { person(id: "00000000-0000-4000-8000-000000000001") { id type siret tva firstName lastName email avatar gender birthDate address { line1 line2 city postalCode state country } phoneNumber note status statusReason statusEndsAt club { id name avatar description address { line1 city postalCode country } billingEnabled } memberships { id planId plan { id name type } expirationDate stripePriceId paymentStatus } attributes { key label type value } } } ``` `club.billingEnabled` is false when the platform turned the club's `billing` module off; the member area then hides prices, credits and payments. `person(id: ID!, clubId: ID)` returns `PersonDetail` or `null` if the person does not exist. When `clubId` is supplied, it also returns `null` if the person does not belong to that club. Both IDs must be valid UUIDs. The basic identity fields and `status` are non-null. Profile fields (`avatar`, `gender`, `birthDate`, `phoneNumber`, and `note`) and `club` are nullable; `address` is a nullable `Address` object with `line1`, `line2`, `city`, `postalCode`, `state`, and `country` fields. `club.address` uses the same nullable `Address` type. `memberships`, `attributes` and `tags` are non-null lists with non-null entries, and may be empty; `tags` is always empty for member sessions. New memberships have a `planId` and expose the matching nullable `plan`; the nullable values preserve historical records created before the plan catalog existed. The plan's `type` distinguishes subscriptions from add-ons. Membership expiration dates and the selected Stripe price ID (`stripePriceId`) are nullable strings. A non-null price identifies a Stripe-backed membership, and multiple memberships may share a price ID. `paymentStatus` is `paid` when a subscription membership's expiration date is today or later, and `unpaid` once that date is in the past. It is null for add-ons, legacy memberships without a catalog plan, and memberships without an expiration date. This billing state is independent from the person's status: a banned member can remain unpaid, and a paid renewal does not lift the ban. Extra-field keys and values are non-null strings. Date fields are exposed as GraphQL strings, not a dedicated date scalar. Query the global [`plans` catalog](/en/graphql/plans) for the full club catalog and Stripe product IDs. `statusReason` is nullable and present only for a scheduled/current suspension, booking block or ban. `statusStartsAt` and `statusEndsAt` are nullable and present only for a scheduled or current suspension or booking block. On `PersonDetail`, `scheduledStatus` names a dated sanction (`suspended` or `no_booking`) that starts later while `status` is still `active`; it is null otherwise. `guestExpiresAt` is the derived ISO expiry of the latest [guest access grant](/en/graphql/guests) and `null` for regular people. A past value marks a former guest. `guestAccess` exposes that latest grant's start, expiry, optional revocation and structured inviter. The inviter kind remains available if the inviter account is deleted, while its ID and current display name become `null`. Kind `external` marks a temporary account created by an [external reservation](/en/graphql/external-reservations); it has no inviter and the person is deleted once the access ends. ### Sponsors and invited guests [#sponsors-and-invited-guests] `personRelations(personId: ID!, clubId: ID!): PersonRelations!` requires the `members_read` permission and returns: * `sponsors`: members who sponsored this person, recorded when their application was accepted; * `sponsoredPeople`: people this person sponsored; * `invitedGuests`: the 50 most recent guest accesses granted by this person, newest first, each with the access `id`, the guest's `personId` and `name`, `startsAt`, `expiresAt` and nullable `revokedAt`. Sponsors and sponsored people contain `id`, `name` and `email`; only people of the requested club are listed. The back-office profile links every entry to its person page, and a guest's **Invited by** links to the inviting member. ```graphql query PersonRelations($personId: ID!, $clubId: ID!) { personRelations(personId: $personId, clubId: $clubId) { sponsors { id name } sponsoredPeople { id name } invitedGuests { personId name expiresAt } } } ``` Determine whether a person has a membership by checking whether `memberships` contains any records. Expired memberships still count as records; existence does not imply an active or paid membership. `peoplePage(isMember: true)` uses the same rule. ## Edit a person's note [#edit-a-persons-note] Send mutations over POST. `updatePersonNote(id: ID!, clubId: ID, note: String!)` updates only the note and returns the updated `PersonDetail`. Both IDs must be valid UUIDs. When supplied, `clubId` is enforced on the database update itself. A missing person or club mismatch returns `null` without changing any record. Omit `clubId` or pass `null` to update without club filtering. ```graphql mutation SavePersonNote($id: ID!, $clubId: ID, $note: String!) { updatePersonNote(id: $id, clubId: $clubId, note: $note) { id note } } ``` `note` is plain text with a maximum length of 10,000 UTF-16 code units. Line breaks and whitespace are preserved. Pass an empty string to clear it (stored and returned as `null`); passing `null` as the mutation input is invalid. Oversized notes and null characters produce `BAD_USER_INPUT`. Notes are not included in directory search or sorting. Notes are available only to authorized club administrators; member reads return null. ## Send a manual notification [#send-a-manual-notification] `sendPersonNotification` lets a club administrator send a received-package, lost-object, or custom message to a member from their profile. See [Member notifications](/en/graphql/notifications#manual-staff-messages). ## Validation errors [#validation-errors] The resolvers reject invalid arguments with GraphQL errors whose `extensions.code` is `BAD_USER_INPUT`: | Condition | Message | | ---------------------------------------------------------------- | ---------------------------------------------------------------- | | Page outside 1 through 1,000,000, or page size not in 10, 25, 50 | `Invalid pagination. Use a positive page and 10, 25 or 50 rows.` | | Search longer than 200 characters | `Search must not exceed 200 characters.` | | Malformed `clubId` or `selectedId` | `Invalid club ID.` | | Malformed person `id` | `Invalid person ID.` | GraphQL also validates argument types and required values before resolver execution. GraphQL rejects non-boolean `isMember` values, including the string `"true"`. Check the response's `errors` array rather than treating every HTTP response as a successful selection. An empty `items` list or a `null` person without errors is a valid no-match result, not a validation failure. ## Membership names and validity [#membership-names-and-validity] `person(id).memberships` returns `id`, `planId`, `planName`, `expirationDate`, and `stripePriceId`. `planName` is the current name of the linked plan, including inactive plans; it is `null` when no plan is linked. `expirationDate` is a nullable `YYYY-MM-DD` date. A missing date does not imply a lifetime membership. ```graphql query MemberMemberships($id: ID!) { person(id: $id) { memberships { id planId planName expirationDate stripePriceId } } } ``` This adds no arguments, filtering, sorting, or pagination changes. ### Profile photos [#profile-photos] People list and detail queries expose nullable `avatar`. To upload or replace a photo, pass `avatar` to `updatePerson(input: ...)` as a PNG or JPEG base64 data URL (maximum 512,000 decoded bytes). The API checks the format, file signature and size. Pass `null` to remove the photo; omit the field to preserve it when editing other details. The photo is stored on the person and is also displayed by linked applications. The clickable avatar uses `updatePersonPhoto(id: ID!, clubId: ID!, avatar: String)`. It returns `PersonResult` and updates only the photo, preserving all other personal details. It accepts the same image format/size rules; a null avatar removes the photo. ## Membership history [#membership-history] The staff-only query `membershipHistory(personId: ID!, clubId: ID!, page: Int! = 1)` returns `{ items, page, hasMore }`, or null when the person does not belong to the requested club. Each page contains at most 20 events, newest first, ordered by `occurredAt` and `id`. `page` must be between 1 and 100,000. New events can shift offset pages; refresh page 1 to see recent changes. Items expose `id`, `membershipId`, `action` (`granted`, `updated`, `renewed`, `removed`), `occurredAt`, `actorType` (`staff`, `member`, `system`), nullable `actorId`, `actorName`, `reason`, and nullable `before`/`after` snapshots. Snapshots contain `planId`, `planName`, `amount` (minor units), `currency`, `interval`, `expirationDate`, `paidPeriodStart` and `paidPeriodEnd`. Dates are ISO strings; missing values are null. `AssignMembershipInput` and `RevokeMembershipInput` accept an optional `reason` (trimmed, at most 2,000 characters, no NUL). The server supplies actor identity from the staff session. History is recorded atomically with the membership change and survives membership removal. Plan details are snapshotted when the event is written. Identical manual grants and ignored Stripe replays do not add events. Paid renewals and add-on fulfilment are attributed to the system. Existing seed data and changes made before this feature have no reconstructed history. Membership, status and attendance histories are views over the shared activity log described in [Staff activity history](/en/graphql/auth#staff-activity-history). The back-office person page displays this history and its pagination. Removing a membership still only removes local access; it does not cancel a Stripe subscription. Neither the consumer nor an administrator of another club can read this staff journal. ## Email consistency [#email-consistency] Staff profile edits update the profile email and an existing member login email in one transaction. A conflict in either table rolls back the entire edit. Photo-only edits do not change login details. The new email is used for subsequent sign-ins and password recovery; a reset link issued to a different email cannot change the password after the update. Existing sessions and passwords are preserved. Staff should verify the replacement address before saving; this operation does not send a new email-verification challenge. An accepted applicant choosing their first password uses the current profile email. Likewise, an activation link sent before a staff email change cannot activate the account; `resendPersonActivation` sends a new link to the current address. Both profile and member-credential tables enforce per-club uniqueness on trimmed, case-insensitive emails, including writes outside the API. Different clubs may reuse an email. Schema installation fails if existing rows conflict; it does not merge or delete people automatically. ## Membership validity filter [#membership-validity-filter] `peoplePage(membershipValidity: current | expired)` supplements the legacy `isMember` filter. `current` means at least one subscription with an expiry on or after today (UTC), or no expiry. `expired` means at least one subscription and no current subscription. Add-ons alone do not satisfy either filter. A person with both an expired and a current subscription appears only under `current`. These filters describe membership dates, not payment settlement or account access status. A suspended person can still have a current subscription. If supplied together with `isMember`, the filters combine with AND. The back-office Membership selector offers Valid membership, Expired memberships only, Any membership record, and No membership records. The last two retain the `isMember` semantics, including add-ons. The URL stores `membershipValidity` for the date filters. ## Status history [#status-history] `personStatusHistory(personId: ID!, clubId: ID!, page: Int! = 1)` is staff-only and club-scoped. It returns null for an unavailable person, otherwise `{ items, page, hasMore }` with 20 entries per page, newest first. Each entry contains `id`, `occurredAt`, `actorId`, `actorName`, `previous` and `current`. Both snapshots contain `status`, `reason`, `startsAt` and `endsAt`. Actor identity comes from the authenticated staff session, not mutation input. Changes to status, reason or suspension dates are recorded atomically with the profile update. Identical repeated writes do not add events. Reactivation clears the current profile reason and dates but preserves them in history. Scheduled suspensions retain their configured dates: the automatic effective-status change at a date boundary is not a new staff action and does not produce a separate event. Earlier actions are not reconstructed. The visible profile reloads this history every 30 seconds and on tab focus; successful status edits refresh it automatically. This journal does not change billing behavior. ## Profile history [#profile-history] `personProfileHistory(personId: ID!, clubId: ID!, page: Int! = 1)` is staff-only and club-scoped, and requires `members_read` in the person's club. It returns null for an unavailable person, otherwise an `ActivityPage` `{ items, page, hasMore }` with 20 entries per page, newest first. ```graphql query PersonProfileHistory($personId: ID!, $clubId: ID!, $page: Int!) { personProfileHistory(personId: $personId, clubId: $clubId, page: $page) { page hasMore items { id occurredAt action actorName changes { field before after } } } } ``` Each entry is one recorded edit of the person. `action` is one of `person.created`, `person.updated` (personal details, legal status or additional information), `person.photo_updated`, `person.note_updated` and `person.tags_updated`. `changes` lists each modified field with its value `before` and `after`; it is empty for creation, photo and note entries, and the note text is never included. `actorName` is the staff member who made the edit, or null for an automatic change. Status and membership changes are not repeated here: use `personStatusHistory` and `membershipHistory`. Edits that change nothing add no entry. ## Signed documents [#signed-documents] The back-office profile lists completed document-signature steps from the person's linked application, including both initial questions and post-approval steps. The page automatically refreshes this section every 30 seconds and on tab focus. Staff with `members_read` in the person's club (club administrators included; see [staff permissions](/en/graphql/auth#individual-staff-permissions)) may list and download documents; anonymous, applicant and member sessions, and staff without that permission, cannot use these operations. ```graphql query SignedDocuments($personId: ID!, $clubId: ID!) { personSignedDocuments(personId: $personId, clubId: $clubId) { configured items { id name signedAt } } } query SignedPdf($personId: ID!, $clubId: ID!, $id: ID!) { downloadPersonSignedDocument(personId: $personId, clubId: $clubId, id: $id) { filename pdf error } } ``` `personId` and `clubId` must be UUIDs. `id` is the opaque document ID returned by the list (1–256 characters). Listing matches both the person's and application's club. It returns an empty list when no linked signatures match. Items use the application's document-name snapshot and the server-recorded completion date, sorted newest first, with no filters or pagination (bounded by onboarding steps). Existing server-confirmed Yousign answers remain readable; new completions also persist the provider request ID separately from the answer's display text. `configured` indicates whether Yousign credentials are available. Listing does not contact Yousign and remains available without credentials. On download the server verifies that the provider request belongs to this club, application and step and has reached `done`. It requests `version=completed`, never the unsigned original, and accepts only a PDF up to 20 MiB. The provider timeout is 25 seconds. Successful responses contain the base64 PDF and a sanitized `*-signed.pdf` filename; errors return null PDF/filename and a safe retry message. Credentials and provider IDs are not returned. Ownership is checked again after the provider responds. PDFs are retrieved on demand from Yousign; this is not a separate document archive. A removed provider document or unfinished request remains unavailable for download. Testing a real download requires a configured Yousign account and a finalized signature request; fixtures do not prove provider access. ## Club credits [#club-credits] See [Club credits](/en/graphql/credits) for credit packs, balances, refill history, credit-priced add-ons and reservations, and cancellation refunds. Membership subscriptions stay billed in money. # Member attributes Club-defined typed person attributes, how onboarding answers save to them, and how staff edit and filter them. Source: https://docs.champagne.dev-ukab.fr/en/graphql/person-attributes Each club defines its own list of member attributes. An attribute has a stable `key`, a `label`, a fixed `type` and, for choices, a list of `options`. Values live on the person record under the key, as typed JSON values. Onboarding questions can save their answer to an attribute or to a built-in person field, so renaming a question or asking the same thing in several onboardings never splits the data. ## List attribute definitions [#list-attribute-definitions] ```graphql query PersonAttributes($clubId: ID! = "51000000-0000-4000-8000-000000000051") { personAttributes(clubId: $clubId) { id key label type options } } ``` `personAttributes(clubId)` returns the club's definitions in creation order, then by label. Any staff member with access to the club can read them; they are used by the settings page, the onboarding editor, the people filters and the person editor. | Type | Stored JSON value | Canonical text in the API | | --------- | -------------------------- | -------------------------- | | `TEXT` | String, up to 10,000 chars | The text | | `NUMBER` | Number | Plain decimal, e.g. `42.5` | | `DATE` | `YYYY-MM-DD` string | `YYYY-MM-DD` | | `BOOLEAN` | `true` or `false` | `true` or `false` | | `CHOICE` | One of `options` | The option | Numbers accept up to 15 integer digits and 6 decimals; a decimal comma is accepted on input and stored as a number. ## Manage definitions [#manage-definitions] ```graphql mutation CreatePersonAttribute($input: CreatePersonAttributeInput!) { createPersonAttribute(input: $input) { attribute { id key label type options } error } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "key": "preferred_language", "label": "Preferred language", "type": "CHOICE", "options": ["French", "English"] } } ``` Keys start with a lowercase letter and contain up to 40 lowercase letters, digits or underscores; they are unique per club. Labels contain 1–100 trimmed characters. `CHOICE` attributes need 2–50 distinct options of 1–100 characters; other types take no options. A key already used in the club returns `error`. `updatePersonAttribute(input: { clubId, id, label, options })` changes the label and, for choices, the options. The key and type cannot change. Updated options are copied into every onboarding question of the club that saves to this attribute. Existing person values are kept even when their option is removed. `deletePersonAttribute(clubId, id)` returns an `error` naming the onboardings that still save to the attribute. Otherwise it deletes the definition and removes the value from every person of the club. These three mutations require the `settings_manage` permission and record `club.attribute_created`, `club.attribute_updated` or `club.attribute_deleted` activity. ## Read and edit a person's values [#read-and-edit-a-persons-values] `PersonDetail.attributes` lists every attribute defined by the person's club, in definition order, with `key`, `label`, `type`, `options` and the person's `value` as canonical text, or `null` when unset. People without a club have no attributes. ```graphql mutation SetPersonAttributeValues($input: SetPersonAttributeValuesInput!) { setPersonAttributeValues(input: $input) { person { attributes { key value } } error } } ``` ```json { "input": { "id": "00000000-0000-4000-8000-000000000084", "clubId": "51000000-0000-4000-8000-000000000051", "values": [ { "key": "newsletter", "value": "false" }, { "key": "profession", "value": null } ] } } ``` Only the listed keys change; `null` or blank clears a value. Each value is validated against the attribute's type and the first invalid one returns `error` such as `Shoe size: Enter a number.` without saving anything. At most 100 values are accepted at once and each key may appear once. The mutation requires `members_manage` and records a `person.updated` activity whose changes are keyed by attribute label. ## Save onboarding answers [#save-onboarding-answers] Short answer, long answer and options questions accept either `personField` or `attributeKey` on `OnboardingQuestionInput` (never both). The target fixes the question type and the applicant's input format, returned as `OnboardingQuestion.format`: | Target | Question type | `format` | Options | | ------------------- | ----------------------------- | --------- | -------------------------------------- | | `PHONE_NUMBER` | `SHORT_ANSWER` | `PHONE` | None | | `BIRTH_DATE` | `SHORT_ANSWER` | `DATE` | None | | `GENDER` | `OPTIONS` | `GENDER` | `female`, `male`, `nonbinary`, `other` | | `TEXT` attribute | `SHORT_ANSWER`, `LONG_ANSWER` | `TEXT` | None | | `NUMBER` attribute | `SHORT_ANSWER` | `NUMBER` | None | | `DATE` attribute | `SHORT_ANSWER` | `DATE` | None | | `BOOLEAN` attribute | `OPTIONS` | `BOOLEAN` | `true`, `false` | | `CHOICE` attribute | `OPTIONS` | `CHOICE` | The attribute's options | For options targets the server replaces the submitted options with the target's. Two questions of one onboarding cannot save to the same target, and an unknown key or an incompatible question type returns `error` from `saveOnboarding`. Questions without a target keep `format: TEXT` (`CHOICE` for options) and their answers stay on the application only. Applicants' answers are validated by format when each step is saved: numbers, real `YYYY-MM-DD` dates, phone numbers of 4–50 digits, spaces, `+`, `(`, `)`, `.` or `-`, and birth dates not in the future. Numbers are stored in canonical form. On acceptance, each answer is written to its target: `phoneNumber`, `birthDate` and `gender` columns, or the attribute under its key as a typed value. Answers whose attribute was deleted since the application started are skipped. ## Filter people [#filter-people] `peoplePage(attributes: [PersonAttributeFilter!])` combines up to 20 filters with the other people filters. Each filter names a `key` and: * `TEXT`: `value` matches a case-insensitive substring; * `BOOLEAN` and `CHOICE`: `value` matches exactly (`true`/`false` for booleans); * `NUMBER` and `DATE`: inclusive `from` and `to` bounds, either optional; * without a value or bound, the filter keeps people who have any value. An unknown key matches nobody. Values and bounds are limited to 200 characters. ```json { "clubId": "51000000-0000-4000-8000-000000000051", "attributes": [ { "key": "preferred_language", "value": "French" }, { "key": "newsletter", "value": "true" } ] } ``` # Member tags Staff-only club tags on people and the people filter. Source: https://docs.champagne.dev-ukab.fr/en/graphql/person-tags Each club keeps its own catalog of tags. A tag has a `label`, unique per club ignoring case, and a `color` from a fixed palette. Tags are applied to any person of the club (members, prospects, guests) and are visible to staff only: member and applicant sessions always read an empty `tags` list, and the member site never shows them. ## Permissions [#permissions] | Operation | Permission | | ------------------------------------------------------- | --------------------- | | `personTags` | Any staff of the club | | `createPersonTag`, `updatePersonTag`, `deletePersonTag` | `tags_manage` | | `setPersonTags` | `tags_manage` | Club administrators hold this permission. Staff accounts receive it individually from the team page. ## List tags [#list-tags] ```graphql query PersonTags($clubId: ID! = "51000000-0000-4000-8000-000000000051") { personTags(clubId: $clubId) { id label color personCount } } ``` Tags are returned ordered by label, ignoring case. `personCount` is the number of people of the club carrying the tag. `color` is one of the 14 palette names: `gray`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `blue`, `indigo`, `violet` or `pink`. ## Manage the catalog [#manage-the-catalog] ```graphql mutation CreatePersonTag($input: CreatePersonTagInput!) { createPersonTag(input: $input) { tag { id label color personCount } error } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "label": "VIP", "color": "orange" } } ``` `UpdatePersonTagInput` takes `clubId`, `id`, `label` and `color`; renaming keeps every assignment. `deletePersonTag(clubId, id)` removes the tag from every person at once. Each result returns the tag or a user-facing `error` string: * `Another tag already uses this label.` — labels are compared trimmed and ignoring case. * `This tag is unavailable.` — the tag does not belong to the club or was deleted. Labels contain 1–24 characters without line breaks; `color` defaults to `gray`. ## Tag people [#tag-people] ```graphql mutation SetPersonTags($input: SetPersonTagsInput!) { setPersonTags(input: $input) { person { id tags { id label color } } error } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "personId": "00000000-0000-4000-8000-000000000001", "tagIds": ["7a000000-0000-4000-8000-000000000001"] } } ``` `setPersonTags` replaces the complete tag list of one person and returns the refreshed `PersonDetail`. An unknown tag or a person outside the club returns an `error` and changes nothing. Every change is recorded in the staff activity as `person.tags_updated` with the previous and new tag labels. Tags are only edited from a person's profile; the people list displays them and filters on them. ## Read tags on people [#read-tags-on-people] `Person.tags` and `PersonDetail.tags` return `[PersonTagSummary!]!` (`id`, `label`, `color`) ordered by label. `peoplePage(tagIds: [ID!])` keeps the people carrying **at least one** of the given tags; it requires `clubId` and accepts up to 50 tags. The back office stores the filter in the URL as repeated `tag` parameters. `peoplePage(untagged: true)` keeps only people carrying no tag at all (URL `untagged=1`); combined with `tagIds` it matches nobody. ```graphql query TaggedPeople($clubId: ID!, $tagIds: [ID!]) { peoplePage(clubId: $clubId, tagIds: $tagIds, page: 1, pageSize: 10) { items { id firstName lastName tags { id label color } } total } } ``` ## Validation errors [#validation-errors] Invalid IDs, labels, colors and oversized tag or people lists are rejected with `BAD_USER_INPUT` before any write. Business outcomes such as duplicate labels or unavailable tags are returned in the `error` field with a null record. # Plans List and manage club plans synchronized with a connected Stripe account. Source: https://docs.champagne.dev-ukab.fr/en/graphql/plans Stripe provider failures return a masked `INTERNAL_SERVER_ERROR` with a server-generated `extensions.requestId`. Use that ID to correlate the sanitized server diagnostic. Expected business validation errors keep their public codes; provider messages and payloads are never returned to clients. The `plans` query returns active plans. Pass `clubId` to list only plans mapped to the selected Stripe merchant for that club. Omitting it retains the global active catalog used by the public application flow. Plans replaced by a [split](#split-a-plan) are omitted unless `includeReplaced: true`; the back-office Plans page requests them. ```graphql query Plans($clubId: ID) { plans(clubId: $clubId) { id name description amount currency interval type stripeId subscriberPrices { amount subscribers } } } ``` The response is a non-null list with non-null entries: ```json { "data": { "plans": [ { "id": "60000000-0000-4000-8000-000000000001", "name": "Adhésion 51 IÉNA (2026) - Abonnement Membre 2026", "description": "Adhésion annuelle donnant accès aux services et événements réservés aux membres.", "amount": 180000, "currency": "eur", "interval": "year", "type": "subscription", "stripeId": "champagne_iena_membre_2026", "subscriberPrices": [ { "amount": 150000, "subscribers": 4 }, { "amount": 180000, "subscribers": 12 } ] } ] } } ``` ## Return fields [#return-fields] | Field | Type | Meaning | | ------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | `ID!` | Internal UUID for the plan and membership relationship. | | `name` | `String!` | Display name. | | `description` | `String` | Optional plan description. | | `amount` | `Int!` | Nonnegative price: currency minor units for memberships, credit hundredths for add-ons, euro cents for credit packs. | | `currency` | `String!` | Lowercase three-letter currency code. | | `interval` | `String!` | Billing interval: `month` or `year`. | | `creditAmount` | `Int` | Fixed pack quantity in hundredths of a credit; required for credit packs, null for other plans. | | `type` | `PlanType!` | `subscription`, `addon`, or `credit_pack`. | | `stripeId` | `String` | Optional catalogue reference; not a universal connected Product/Price ID. | | `subscriberPrices` | `[PlanSubscriberPrice!]` | Memberships with a Stripe subscription and a running paid period on this plan, grouped by the `amount` they currently pay and ordered by amount. Populated by `plans` for staff only; `null` without a staff session, on plans returned by mutations or nested in memberships. | | `replacedBy` | `PlanReplacement` | `{ id, name }` of the plan that replaced this one in a [split](#split-a-plan); `null` otherwise. Populated by `plans`. | Stripe price references belong to memberships rather than plans. See the [`person` query](/en/graphql/people) for membership fields. The back office converts `amount` from minor units for display. The back office always supplies its selected club ID. The editor offers searchable, localized currency names from Stripe's card presentment currency list; availability can still depend on the connected account and payment method. It preserves the entered major-unit price when changing the currency and rejects unsupported fractional amounts rather than rounding them. For example, 1,200 JPY is sent as `amount: 1200`, while 12 EUR is sent as `amount: 1200`. Stripe's ISK and UGX compatibility rules still use a factor of 100 but only permit whole major-unit amounts. The back-office table can sort its returned plans by name, type, amount, or billing interval in either direction. This sorts the loaded list (up to 100 plans); the GraphQL `plans` query still returns its default name/ID order. ## Create and update [#create-and-update] ```graphql mutation CreatePlan($input: PlanInput!) { createPlan(input: $input) { id name amount currency interval type } } ``` `PlanInput` contains `clubId`, a retry-stable UUID `requestId`, `name`, optional `description`, nonnegative minor-unit `amount`, lowercase three-letter `currency`, `interval` (`month` or `year`) and `type` (`subscription`, `addon`, or `credit_pack`). Names are trimmed and limited to 200 characters; descriptions are limited to 1,000 characters; amounts cannot exceed `100000000` minor units. `createPlan` creates a Product and recurring Price on the club's selected connected Stripe account before saving their local mapping. Retrying the same request uses the same Stripe idempotency keys and local plan ID. ```graphql mutation UpdatePlan($id: ID!, $input: PlanInput!) { updatePlan(id: $id, input: $input) { id name amount currency interval } } ``` `updatePlan` updates the plan's name and description and its Stripe Product. A plan's `currency`, `interval` and `type` are fixed once it exists, and so is a subscription's `amount`: an update that changes any of them returns a `BAD_USER_INPUT` error before any Stripe call, and the back-office editor only shows those fields when creating a plan. To give a subscription a new price, [split it](#split-a-plan). Add-ons and credit packs keep an editable `amount`. Stripe Prices are immutable, so a new amount creates a replacement Price, updates the local mapping and archives the previous Price. Subscriptions keep renewing on the Price they were created with: every Price created for a plan is recorded, and renewals are validated against the Price the member is subscribed to. ### Fields that cannot be updated [#fields-that-cannot-be-updated] Configuration tools that compare a declared plan with the stored one cannot call `updatePlan` when one of these fields changes: | Field | Subscription | Add-on, credit pack | | ---------- | ------------------------------- | ---------------------------------------- | | `amount` | [Split the plan](#split-a-plan) | Updated in place, with a new `requestId` | | `type` | Delete, then create | Delete, then create | | `currency` | Delete, then create | Delete, then create | | `interval` | Delete, then create | Delete, then create | `name`, `description` and a credit pack's `creditAmount` are always updated in place. An in-place `amount` change creates a Stripe Price under the update's `requestId`, so that id must differ from the ones already used for the plan. A subscription changes price with `splitPlan`, passing its current amount as `subscriberAmount`: its subscribers keep the plan and the price they pay, and the new plan takes its place for everyone else. A change of `type`, `currency` or `interval` needs the plan deleted and created again, which [is refused](#delete) while members hold it. ## Read one plan [#read-one-plan] ```graphql query Plan($clubId: ID!, $id: ID!) { plan(clubId: $clubId, id: $id) { id name description amount creditAmount currency interval type } } ``` Returns one plan offered by the club's selected merchant, including a plan replaced by a split, or null. Like `plans`, it is public. Both ids must be UUIDs. `subscriberPrices` is null unless the caller manages the catalog of that club. ## Split a plan [#split-a-plan] Splitting gives new and current subscribers different prices. The split plan keeps its name, ID and memberships and is reserved to its current subscribers; a new plan with a name chosen by staff takes its place for everyone else. ```graphql mutation SplitPlan($input: SplitPlanInput!) { splitPlan(input: $input) { plan { id name amount } previous { id name amount } scheduled } } ``` ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "planId": "60000000-0000-4000-8000-000000000001", "requestId": "7f7a2b3c-1d2e-4f50-8a6b-9c0d1e2f3a4b", "name": "Adhésion 51 IÉNA (2027) - Abonnement Membre", "amount": 200000, "subscriberAmount": 190000 } } ``` `SplitPlanInput` contains `clubId`, the subscription `planId` to split, a retry-stable UUID `requestId` (also the new plan's ID), the new plan's trimmed `name` (1–200 characters) and optional `description`, and two minor-unit prices (`0`–`100000000`): `amount` for new subscribers and `subscriberAmount` for current subscribers. Currency and interval are copied from the split plan. Only subscription plans can be split, and a plan can be split once; retrying the same `requestId` returns the same result. `splitPlan` returns the new `plan`, the retained `previous` plan and the number of current subscribers whose price change was `scheduled`. It: * creates a Stripe Product and Price for the new plan and maps it to the club's merchant; * keeps the split plan's name, sets its `amount` to `subscriberAmount` and links it to the new plan through `replacedBy`. When `subscriberAmount` differs from its current amount, a new Price is created on the split plan's Product; * replaces the split plan with the new plan in every `PLAN_SELECTION` block of the club's onboardings and increments their revision. Blocks offering all plans need no change; * hides the split plan from `plans` (unless `includeReplaced: true`), from `membershipOffers`, from Checkout and from application plan selection; * schedules the price change for current subscribers, as described below, and records `plan.split` in staff activity with `replacedBy`, `newSubscriberAmount`, `subscriberAmount`, `subscribersRepriced` and `subscriberPriceFrom`. Current subscribers are memberships of the split plan with a Stripe subscription, a paid period that has not ended, and a Price in the same currency and interval. Members already paying `subscriberAmount` are left unchanged. The others get the new Price scheduled one calendar month later and receive a `renewal` notification (in-app and email, per their preferences) with the old and new amounts and the renewal date from which the new price is charged. The new price applies from the member's next payment, or one month after the split if that payment is sooner. The notification states that date. A background worker switches each subscription item to the scheduled Price once the notice month has passed: * if the subscription has not renewed since the split, the switch uses `proration_behavior: "none"` and the next renewal charges the new price; * if it renewed during the notice month (at the old price), the switch uses `proration_behavior: "always_invoice"`, so Stripe immediately invoices the prorated difference for the rest of the current period (a credit when the price goes down). Proration invoices do not change the membership's paid period. Subscriptions that have ended in Stripe are dropped from the schedule. Applications that already selected the split plan but have not paid can no longer check out with it. ## Delete [#delete] ```graphql mutation DeletePlan($id: ID!, $clubId: ID!) { deletePlan(id: $id, clubId: $clubId) } ``` A plan members still hold cannot be deleted: the mutation answers `BAD_USER_INPUT` with `Members still hold this plan. End or move their memberships before deleting it.` and archives nothing. A member holds a plan while their membership has no end date, has not expired, or is inside a paid period; ended memberships do not count. This applies to a plan replaced by a split too, for as long as its subscribers stay on it. Otherwise deletion archives the connected Price and Product, removes the merchant mapping and hides the local plan. Historical plans referenced by memberships or checkout attempts remain stored. If several clubs share the same merchant profile, they share its plan catalog too. ## Ordering and limits [#ordering-and-limits] Plans are ordered by name and then ID. The query returns at most 100 records and does not accept sorting or pagination arguments. An empty catalog returns `[]`. The plan query is public for application flows. Plan mutations (`createPlan`, `updatePlan`, `deletePlan` and `splitPlan`) require a staff session that administers the selected club or holds its `catalog_manage` permission; see [staff permissions](/en/graphql/auth#individual-staff-permissions). Mutations require configured sandbox Stripe billing and a selected merchant. Checkout resolves active subscription Product/Price mappings by club → selected merchant → local plan. Use [`membershipOffers(clubId)`](/en/graphql/billing) for purchasing readiness and offers. ## Club credits [#club-credits] See [Club credits](/en/graphql/credits) for credit packs, balances, refill history, credit-priced add-ons and reservations, and cancellation refunds. Membership subscriptions stay billed in money. Credit packs require a positive EUR amount and create a one-time Stripe price. Their stored `interval` is ignored for billing. Rate settings determine the credits granted, snapshotted when Checkout starts. Add-ons are also purchased once with credits; their interval is not a recurring charge. Credit packs require `creditAmount` in `PlanInput` (1–100,000,000 hundredths). For example, `amount: 10000, creditAmount: 125000` sells 1,250 credits for €100. Changing the club rate does not change this quantity. # Platform administration Spaces, clubs and owners managed by platform admins. Source: https://docs.champagne.dev-ukab.fr/en/graphql/platform Platform admins run the Champagne instance itself. They create a space for each customer, add its clubs and choose its owner. The role is a flag on the staff account (`staffViewer.isPlatformAdmin`); it gives no access to the data of any club. A platform admin who also works in a club needs club access like any other staff member. The demo seed creates the platform admin `superadmin@example.com`, who has no club. Platform admins then give the role to others with `grantPlatformAdmin`. ## Permissions [#permissions] | Operation | Permission | | ------------------------------------------------------------------------------------------------------ | -------------- | | `platformSpaces` | Platform admin | | `platformAdmins`, `platformActivity`, `grantPlatformAdmin`, `revokePlatformAdmin` | Platform admin | | `createPlatformSpace`, `createPlatformClub`, `setPlatformSpaceOwner`, `cancelPlatformOwnerInvitation` | Platform admin | | `renamePlatformSpace`, `updatePlatformClub`, `setClubStatus`, `setClubFeatures`, `setClubPhoneCodeCap` | Platform admin | | `setPlatformSpaceArchived` | Platform admin | \| `platformApiTokens`, `createPlatformApiToken`, `setPlatformApiTokenBlocked`, `deletePlatformApiToken` | Platform admin | Club admins and space owners are refused with `UNAUTHENTICATED`. A [platform API token](/en/graphql/api-tokens#platform-tokens) is limited to `platformSpaces`, `createPlatformClub`, `updatePlatformClub`, `setClubStatus` and `setClubFeatures`. ## List spaces [#list-spaces] ```graphql query PlatformSpaces { platformSpaces { id name owner { id name email } pendingOwner { email expiresAt expired removesPreviousOwner } clubs { id name timezone hostedSlug status disabledFeatures } } } ``` Spaces are ordered by name, and their clubs by name. `owner` is null until an invited owner accepts. `pendingOwner` is the owner invitation waiting to be accepted, for a new space or a transfer. `expiresAt` is when its link stops working, 7 days after it was sent, and `expired` turns true from then on: the invitation must then be sent again. `removesPreviousOwner` tells whether the current owner loses their access once it is accepted. ## Create a space [#create-a-space] ```graphql mutation CreatePlatformSpace($input: CreatePlatformSpaceInput!) { createPlatformSpace(input: $input) { id owner { email } pendingOwner { email } } } ``` ```json { "input": { "name": "Riviera group", "ownerEmail": "owner@riviera.example", "clubs": [{ "name": "Riviera Nice", "timezone": "Europe/Paris", "hostedSlug": "riviera-nice" }] } } ``` The space and its clubs are created in one transaction. Each club starts with the default settings and onboarding of any new club. * If `ownerEmail` matches an existing staff account, that account becomes owner at once and admin of every club in the space. * Otherwise the email receives an invitation to the back office's `/invitation` page. Accepting it creates the account (or reuses one created since), makes it owner and admin of every club in the space. The invitation never appears in the space's team list. If the email cannot be delivered, the space stays created, the invitation is withdrawn, and the mutation fails with a message saying so. Send the invitation again with `setPlatformSpaceOwner`. ## Add a club [#add-a-club] ```graphql mutation CreatePlatformClub($spaceId: ID!, $input: PlatformClubInput!) { createPlatformClub(spaceId: $spaceId, input: $input) { clubs { id name } } } ``` The owner, if there is one, becomes admin of the new club. Other staff get access from the team page as usual. ## Change the owner [#change-the-owner] ```graphql mutation SetPlatformSpaceOwner($spaceId: ID!, $email: String!, $removePreviousOwner: Boolean) { setPlatformSpaceOwner( spaceId: $spaceId email: $email removePreviousOwner: $removePreviousOwner ) { owner { email } pendingOwner { email } } } ``` The same rule applies: an existing account takes over at once, anyone else is invited. The previous owner stays owner until the invitation is accepted. Calling it again with the pending email sends a fresh invitation and withdraws the previous one. `removePreviousOwner` defaults to `true`: when the new owner takes over (at once for an existing account, on acceptance for an invitation), the previous owner loses every club access, reservation assignment and their place in the space, as if removed from the team. Pass `false` to keep them as admin of the clubs. ## Cancel an owner invitation [#cancel-an-owner-invitation] ```graphql mutation CancelPlatformOwnerInvitation($spaceId: ID!) { cancelPlatformOwnerInvitation(spaceId: $spaceId) { pendingOwner { email } } } ``` Withdraws the owner invitation waiting on the space; its link stops working at once. The current owner, if any, is unchanged. ## Suspend a club [#suspend-a-club] ```graphql mutation SetClubStatus($clubId: ID!, $status: ClubStatus!) { setClubStatus(clubId: $clubId, status: $status) { clubs { id status } } } ``` `status` is `active` or `suspended`. Suspending keeps every record of the club, but from the next request: * the club disappears from the `clubIds` and `clubRoles` of its staff sessions, so its operations are refused, and staff whose only club it was see the no-club page; * member sessions of the club no longer resolve, and `loginMember` is refused with `This club is suspended. Contact the club for more information.`; * the club leaves `clubDirectory`, and `hostedClub` returns null for its hosted address; * it leaves `externalBookingClubs`, `externalReservationCatalog` returns null, its availability is empty and `createExternalReservation` is refused; * `onboarding`, `onboardings` and `hostedOnboarding` return nothing, `startApplication` returns `The onboarding is unavailable.`, application recovery answers `This club could not be found.`, and applicant tokens stop granting access to its applications; * `plans` returns no plan for it, and `membershipOffers` and `addonOffers` report `Merchant payments or catalog are not ready.` * background jobs schedule no reservation or renewal reminder for it, skip its notification emails and promote no one from its waitlists; reservation confirmations, application decisions and application reminders wait and go out once the club is reactivated. Refunds, deposit refunds and their emails continue. Setting `active` again restores every access, including existing sessions that have not expired. The people of a closed club are told why it is gone: * `staffViewer.closedClubs` lists the clubs the account works in that are suspended or archived, with their `name` and `status`. The back office shows them on its no-club page. * The public `closedClub(clubId)` query returns the club's `name` and `status` when it is suspended or archived, and null otherwise. The member sign-in page asks it for a remembered or requested club that `clubDirectory` no longer lists. ```graphql query ClosedClub($clubId: ID!) { closedClub(clubId: $clubId) { name status } } ``` ## Archive a club or a space [#archive-a-club-or-a-space] `setClubStatus` with `archived` closes the club exactly like a suspension; the only difference is for the people involved: `loginMember` answers `This club is closed.` and the platform page hides archived clubs until its archives are shown. Restore a club with `status: active`. ```graphql mutation SetPlatformSpaceArchived($spaceId: ID!, $archived: Boolean!) { setPlatformSpaceArchived(spaceId: $spaceId, archived: $archived) { archived clubs { id status } } } ``` Archiving a space sets `archived` and archives every club of the space, suspended ones included. While the space is archived, `createPlatformClub` and `setClubStatus` with any status other than `archived` are refused with `This space is archived. Restore it first.` Restoring the space puts each club back in the status it had when the space was archived: a suspended club stays `suspended`, and a club archived before the space, or archived with `setClubStatus` while the space was archived, stays `archived`. ## Phone code limit [#phone-code-limit] ```graphql mutation SetClubPhoneCodeCap($clubId: ID!, $perHour: Int) { setClubPhoneCodeCap(clubId: $clubId, perHour: $perHour) { clubs { id phoneCodesPerHour } } } ``` `phoneCodesPerHour` is the club's own hourly cap on application phone verification codes, between 1 and 1,000,000, for example raised for an opening day. Null, the default, makes the club follow the deployment's per-club cap (`SMS_HOURLY_CAP_CLUB`, 100 unless configured). Other values are refused with `BAD_USER_INPUT`. The per-number, per-connection and platform caps and the anti-bot check still apply. The change is recorded as `platform.club_phone_cap_changed`. ## Turn modules off [#turn-modules-off] ```graphql mutation SetClubFeatures($clubId: ID!, $disabledFeatures: [ClubFeature!]!) { setClubFeatures(clubId: $clubId, disabledFeatures: $disabledFeatures) { clubs { id disabledFeatures } } } ``` Every module is on for a new club. `disabledFeatures` replaces the list of modules turned off, returned in the order below whatever the order sent. A module turned off removes the staff permissions it covers for every role, club admins included: its back-office sections disappear and its staff operations are refused with `UNAUTHENTICATED`. Member-facing operations, such as booking or chatting from the member area, are not affected, except for `billing` (see [Clubs with billing turned off](#clubs-with-billing-turned-off)). | Module | Staff permissions removed | Sections hidden | | ---------------- | --------------------------------------------------------------------------- | -------------------------------------------- | | `reservations` | `reservations_manage`, `attendance_manage`, `chats_manage`, `planning_read` | Reservations, Club calendar, My reservations | | `availability` | `availability_manage` | My availability | | `customer_chats` | `customer_chats` | Chats, unless reservation messages remain | | `housekeeping` | `housekeeping_manage` | Housekeeping | | `newsletters` | `newsletters_manage` | Newsletters | | `applications` | `applications_manage` | Applications | | `billing` | `billing_manage` | Finance | Reservation categories and resources are configured with `catalog_manage`, which also covers plans, so these operations stay available to admins when `reservations` is off; only the Reservations section is hidden. Staff sessions read the modules on every request: `staffViewer.clubRoles` returns `disabledFeatures` for each club, and its `permissions` no longer list the removed ones. ### Clubs with billing turned off [#clubs-with-billing-turned-off] `billing` also covers members: a club with it off charges nothing. `Club.billingEnabled` is `false` for that club in `clubSettings`, `hostedClub`, `clubDirectory` and a person's `club`, so both apps hide prices, credits, top-ups, plans and payment steps. Every operation that would charge money or credits for the club is refused with `BAD_USER_INPUT` and the message `Payments are turned off for this club.`: | Area | Operations | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Memberships | `createMembershipCheckout`, `createApplicationCheckout`, `createPaymentMethodPortal` | | Credits | `createCreditCheckout`, `configureCredits`, and every credit debit: `payReservationCredits`, `payAddonCredits`, reservation edits that cost more, `reportProductConsumption` | | Reservations | `createReservationCheckout` | | Catalog | `createPlan`, `updatePlan`, `splitPlan` | | Merchants | `createMerchant`, `selectMerchant`, `merchantOnboarding`, `prepareMerchantCatalog` | | Onboardings | `saveOnboarding` with a `PLAN_SELECTION` or `PLAN_PAYMENT` step (returned in `error`) | Bookings stay open and are free: resource prices, plan prices, deposits and product prices count as zero, so new reservations are confirmed without payment, including online bookings. Onboarding reads (`onboarding`, `onboardings`, `hostedOnboarding`) leave out plan selection and payment steps, and new applications start without them. Turning `billing` off with `setClubFeatures` also removes the plan steps that open applications have not reached yet, in the same transaction; an accepted application left with no remaining step is marked complete. A club created with `billing` off gets a default onboarding without them. Turning billing off keeps existing data: invoices, credit balances and history, paid memberships, paid reservations and stored prices are not changed. Refunds of earlier payments still work, so a cancellation or deposit refund returns what the member paid. Stripe webhooks still finalize a payment started before billing was turned off. Turning billing back on restores prices and steps as they were stored. ## Platform admins [#platform-admins] ```graphql query PlatformAdmins { platformAdmins { id name email } } mutation GrantPlatformAdmin($email: String!, $name: String!) { grantPlatformAdmin(email: $email, name: $name) { id } } mutation RevokePlatformAdmin($userId: ID!) { revokePlatformAdmin(userId: $userId) { id } } ``` `platformAdmins` lists the accounts with the role, ordered by name. `grantPlatformAdmin` gives the role to the staff account using `email`, and `name` is then ignored. When no account uses it, a new account is created with `name` (1–100 characters) and an unusable password, and the email receives a link to the back office's `/reset-password` page to choose one, valid for 7 days. If that email cannot be delivered, the new account is deleted and the mutation fails with `The setup email could not be sent, so no account was created. Try again.` `revokePlatformAdmin` removes the role and keeps the account and its club access. Removing your own role is refused with `You cannot remove your own platform access.`, so the platform always keeps at least one admin. ## Platform history [#platform-history] ```graphql query PlatformActivity($page: Int!) { platformActivity(page: $page) { items { occurredAt action actorName subjectType subjectLabel changes { field before after } } hasMore } } ``` Every platform mutation records an entry, newest first, 20 per page. `action` is one of `platform.space_created`, `platform.space_renamed`, `platform.space_archived`, `platform.space_restored`, `platform.club_created`, `platform.club_updated`, `platform.club_status_changed`, `platform.club_modules_changed`, `platform.club_phone_cap_changed`, `platform.owner_changed`, `platform.owner_invited`, `platform.owner_invitation_canceled`, `platform.admin_granted`, `platform.admin_revoked`, `platform.api_token_created`, `platform.api_token_blocked`, `platform.api_token_unblocked` or `platform.api_token_deleted`. `subjectType` is `space`, `club` or `staff`, and `subjectLabel` is its name at the time. `changes` lists the fields that changed: `owner` (emails), `space` (for a new club), `name`, `timezone`, `hostedSlug`, `status` and `disabledFeatures` (comma-separated module ids). These entries belong to no club or space, so they never appear in a club's or a staff member's history. ## Validation [#validation] | Field | Rule | | ------------ | ------------------------------------------------------------------------ | | `name` | Space and club names hold 1 to 100 characters after trimming. | | `ownerEmail` | A valid email address, compared ignoring case. | | `clubs` | 1 to 20 clubs when creating a space. | | `timezone` | An IANA timezone, such as `Europe/Paris`. | | `hostedSlug` | Optional; 3 to 63 lowercase letters, digits or hyphens, unique per club. | Club names are saved with an uppercase first letter, so `riviera Nice` becomes `Riviera Nice`. Names are compared ignoring case: no two spaces share a name, archived ones included, and no two clubs of a space share a name. Invalid input is refused with `BAD_USER_INPUT`. A hosted address already used by another club is refused with the message `This address is already used by another club.`, a taken space name with `Another space already has this name.`, and a club name taken in its space with `Another club of this space already has this name.` # Reservations Member reservation requests, club-scoped categories and staff reservation lists. Source: https://docs.champagne.dev-ukab.fr/en/graphql/reservations ## Floor plan imports [#floor-plan-imports] `startFloorPlanImport`, `floorPlanImport` and `cancelFloorPlanImport` require staff `catalog_manage` permission. UUID `clubId` and `categoryId` scope the category; read/cancel additionally take UUID `id`. Jobs belong to the exact user, club and category. Wrong ownership returns `null`; unauthorized callers are denied before service dispatch. Start checks category existence and visualization availability. Start returns a `BAD_USER_INPUT` error when the image is not a PNG, JPEG or WebP under 6 MB, when an import is already running for the same scope or the server's 24 concurrent imports are in use, or when plan import is not configured. ```graphql mutation ImportPlan($clubId: ID!, $categoryId: ID!, $image: String!) { startFloorPlanImport(clubId: $clubId, categoryId: $categoryId, image: $image) { id status step version } } query ImportProgress($clubId: ID!, $categoryId: ID!, $id: ID!) { floorPlanImport(clubId: $clubId, categoryId: $categoryId, id: $id) { id status step version warnings reviewNotes referenceImage registration { sourceWidth sourceHeight cropX cropY cropWidth cropHeight scale offsetX offsetY } cursor { x y } elements { id spaceId kind label objectId resourceId bookable labelVisible x y width height rotation color strokeColor strokeWidth outlineOnly chairs points { x y arc radius } locked } objects { id name category reservable resourceId image artworkRef } } } mutation CancelImport($clubId: ID!, $categoryId: ID!, $id: ID!) { cancelFloorPlanImport(clubId: $clubId, categoryId: $categoryId, id: $id) { id status } } ``` `image` is a base64 PNG/JPEG/WebP data URL under 6 MB, not a remote URL or SVG. Magic bytes and decoded image validity are checked, including a 16-million-pixel limit. Original pixels remain available for regional source inspection. The first model image is resized to at most 1600 by 1600; no automatic claim is made that phone chrome or annotations have been removed. The configured `OPENROUTER_API_KEY` powers `openai/gpt-6.1-sol`. The loop is bounded by 32 turns, 200 instances, 40 operations per transaction, four custom templates and ten minutes. One import may run per exact scope; at most 24 retained/starting jobs exist per process. Jobs expire after 30 minutes and do not survive restart. Poll every 500 ms while running and stop on terminal state or error. There are no filters, sorting or pagination; queries return one bounded snapshot. Statuses: `running`, `review`, `failed`, `cancelled`. `step` is the actual tool; `version` increments on successful atomic scene transactions, including reorder and deletion. Failed operations do not change geometry, version or cursor. Cursor coordinates identify the last affected instance center (former center for deletion). Cancellation aborts provider/generation requests, prevents late changes and removes scope-matched pending artwork. Cleanup failures are logged; accepted library images are never removed. ### Native scene tools [#native-scene-tools] * `register_source({crop:{x,y,width,height},world:{x,y,width,height}|null})` uses ORIGINAL source pixels and returns a high-resolution crop plus the uniform affine source-to-canvas mapping. It must precede geometry. The optional world box places/scales the source mapping; it is not a placement boundary. Choose a larger proportional mapping when furniture needs more clearance. Registration locks while any instances exist. The aligned raster stays bounded to 1800 pixels per axis. * `view_reference({region:{x,y,width,height}|null})` returns real original-pixel regional PNGs, at most 1800 by 1800, without upscaling small inputs. * `scene_transaction({expected_version,operations:[...]})` validates all operations before publishing. Supported operations are `create`, `update`, `delete`, `reorder`. Create takes `element`; update takes a stable `id` and `element` patch; delete takes `id`; reorder takes `id` and `before_id` (null means front). Duplicate/unknown IDs, stale versions, invalid fields, bounds or resource references reject the entire transaction. Responses include resolved records, created key/ID mappings, deleted IDs, warnings and remaining time/turn budget. Errors include code, field and the operation index where applicable. * `list_objects({})` reports scene version, instance IDs/geometry, the active library with `library_object_id`, name and reservability, registration, resource assignment status, warnings and remaining budget. * `view_objects({region,scale,labels,overlay})` returns a real draft PNG. Region is in WORLD units or null for a dynamic full-scene fit; scale is 1..3; labels are `none` or `names`. Optional internal source overlay uses the exact registered reference at opacity 0.35. PNGs are bounded to 2400 pixels per axis; the response includes `viewport`, `pixels`, `pixelsPerUnit`, `units:world`, and `full`. A null region counts as full inspection regardless of extent. * `scene_transaction` also accepts `{op:"move_layer",id,direction}`. Directions `forward`/`backward` move one layer; `front`/`back` move to the top/bottom within the object's space. Locked layers are rejected; boundary moves are no-ops. Geometry, resource bindings and other spaces are unchanged. Like other transactions, moves require the current `expected_version` and are atomic. Both `list_objects` and transaction results return `layer_order`, listing instance IDs back-to-front (last is topmost). Existing `reorder`/`before_id` remains available for precise positioning. * `create_template({name,shape_description?,reservable})` calls the same name-based AI object generator as the editor. No source crop is accepted or sent. The optional internal description (up to 1,000 characters) specifies silhouette, proportions and physical parts; standard styling remains authoritative. The name is limited to 100 characters. It returns `library_object_id`, reservability, intrinsic dimensions, aspect ratio, geometry format and a preview. Up to four attempts are allowed after early layout inspection and before turn 24. The resulting object joins the draft library and is persisted only on human acceptance. * `finish({expected_version,review_notes})` submits an unsaved draft for human review. It is never a save or an assertion of reconstruction accuracy. Library placement example (a new reservable table proposed for human review): ```json { "expected_version": 0, "operations": [ { "op": "create", "element": { "key": "terrace-table-01", "library_object_id": "builtin:ellipse", "bookable": true, "label": "Terrace table", "x": 615, "y": 203, "width": 47, "height": 47, "rotation": 0, "resource_id": null, "chairs": 4, "outline_only": false, "label_visible": false, "points": null } } ] } ``` The importer and editor share the active builtin library: eight editable reservable shapes and eleven architectural/decorative objects. The agent also receives the current category's saved library objects. Every placement selects one `library_object_id`; separate `kind` and `template_id` arguments are rejected. Deleted or unknown IDs are rejected. Generated objects use this same selector. `key` is an agent-chosen creation correlation key; subsequent edits use the returned instance `id`, not a library ID. Storage and GraphQL retain the editor's existing `kind`/`objectId` rendering fields, resolved internally from library selection. Doors use the builtin door artwork. Walls are prohibited except essential room separation; the model must not surround a plan with perimeter geometry. Shared artwork, paints and fixed outlines are used in the editor, consumer and server inspection. Polygon points use LOCAL 0..100 coordinates with editor arc/radius semantics (up to 64 points); arbitrary SVG and holes are unsupported. Instance coordinates, dimensions, rotation and local anchors preserve fractional world units. Negative positions and expansion beyond the initial 1000 by 700 view are supported. Numeric safety limits are absolute positions at most 1 billion and dimensions at most 1 million, not a fixed canvas. Non-bookable dimensions may be 1 unit; reservables require width >= 20 and height >= 10. Reservables and native furniture with chairs must have at least 12 world units of clearance, including conservative rotated bounds and chair footprints. Insufficient spacing returns `SPACING` and rejects the entire transaction; a larger canvas alone is not a spacing guarantee. Rotation is clockwise about the local center and normalizes -360..360 to 0..359. Native table shapes support 0..16 chairs. Custom template zones retain aspect via contain-fit. Array order is persisted z-order. The import modal omits the reference toggle and crop metadata; agent reference tools remain internal. Scene-version updates trigger one 900ms shine across the preview, not a moving cursor or perpetual pulse. Reduced-motion users get no animation. The editor dynamically fits expanded plans, supports wheel zoom, Alt/middle-button pan and Home to refit; existing selection, snapping, rotation and undo remain. Read-only previews support touch pan and pinch zoom without triggering booking clicks after a drag. Member rendering fits the same world bounds before its existing pinch/pan gestures. ### Bookability and save safety [#bookability-and-save-safety] The agent must place visible dining tables and standalone seats/loungers as reservable proposals. `bookable:true` with `resource_id:null` requests a new resource when the reviewed plan is saved. Native reservable library shapes default to this flag; generated objects declare `reservable` and use the same placement behavior. Decorative objects use `bookable:false`; non-reservable library objects cannot receive a booking binding. A non-null `resource_id` must identify an available resource in this category and requires `bookable:true`. Already-assigned resources cannot be duplicated. An attached dining chair belongs to its table booking unit; a standalone seat is a separate placement. Import generation never writes resources. Persisted `FloorPlanElement` fields now include nullable `bookable`, `labelVisible`, `strokeColor`, `strokeWidth`. Labels are hidden when `labelVisible:false`. Styling, points, chairs, dimensions, rotations, identities and ordering round-trip in plan JSON and are rendered by both frontends. `bookable:false` is rejected with a resource ID. **Only explicit `bookable:true` on a supported reservable shape can create a missing resource during save**; omitted/null/false flags never create resources merely from labels. The editor's manual resource creation/assignment choices explicitly set the flag, and “No booking binding” clears it. The importer preserves existing editor changes. “Use draft” accepts only placed pending artwork through the existing library flow, maps temporary object IDs and adds an undoable new space. When the editor contains exactly one empty space, the draft fills that space and preserves its identity, name and styling. Multiple spaces or any existing objects are preserved and the draft gets a new space. It preserves reservable proposals, including generated library object reservability, checks combined capacity/assignment conflicts, and does not save the plan. “Save plan” still uses `saveReservationFloorPlan` revision/conflict validation. After saving, the selected space remains active in the read-only plan view, including a newly added or imported space. A failed save retains the editor draft and selected space for retry. Closing cancels/discards the import. Test and diagnostic harnesses never save user plans/resources. ### Honest inspection and review [#honest-inspection-and-review] An early full structural inspection must precede finishing or generating custom templates. Three mutation batches require an inspection before further edits. Finishing requires two distinct regional reviews and a full, non-overlay PNG of the latest version after all corrections. Changing geometry/order invalidates the previous full inspection. Four final turns and a three-minute time reserve are recommended explicitly in model feedback; late custom generation is prohibited. Duplicate geometry and unintended bookability prevent finishing. Unplaced created templates are detected/warned and not saved. `reviewNotes` must describe remaining omissions/uncertainty and appears in the review modal. `warnings` gives objective scene hygiene findings. Receiving a PNG alone does not establish visual fidelity: the model is instructed to compare regions, correct discrepancies and avoid inventing clipped-off geometry. The human still reviews before acceptance and explicit save. The source crop and overlay are private transient review data, not saved into or published with consumer plans. ## Reservation access [#reservation-access] Reservation category management and direct booking require an owner or admin session. Staff can list, edit, accept, cancel, propose times for, and chat on reservations only when their account is assigned to that reservation category. Their back office exposes this restricted workflow as Planning. Members may read their own club's categories and request a reservation for themselves. They may also read their own upcoming reservations through `myReservations`; the authenticated person identity always comes from the member session. Anonymous callers and callers from another club receive `UNAUTHENTICATED`. Members can read only their own club's IANA timezone with `memberClubTimezone`: ```graphql query MemberClubTimezone($clubId: ID!) { memberClubTimezone(clubId: $clubId) } ``` The required `clubId` is a UUID matching the member session; staff and unauthenticated callers cannot use this query. It returns `null` when the club no longer exists and otherwise returns its configured timezone (Europe/Paris by default). No other club settings are returned. Reservation timestamps remain UTC ISO instants; members display and enter civil times in this club timezone, including across daylight saving changes. The back office starts with a persisted reservation-category list. Selecting a category opens its configuration, upcoming reservations, and calendar. Internally, validated session inputs retain their schedule variant: weekly sessions require a weekday, start time and duration; daily sessions require a start time and duration; custom sessions require start and end dates. Category session drafts use the same contract. Nullable database rows are validated before occurrence expansion; an invalid schedule produces no bookable occurrence. ## Service failures [#service-failures] Empty lists and `null` results represent domain outcomes returned by the reservation service. They are not substitutes for a missing implementation. All methods in the internal `ReservationServices` contract are required at composition time. Unexpected service failures use the standard masked GraphQL error (`Unexpected error.`), rather than a successful empty response. Authentication and input validation remain unchanged. ## Reservation categories [#reservation-categories] ```graphql query ReservationCategories($clubId: ID!) { reservationCategories(clubId: $clubId) { id clubId name description conversationRequired createTicket resources { id name amount priceUnit depositAmount defaultCheckInMinutes defaultCheckOutMinutes cancellationNoticeDays staffUserIds planPrices { planId amount } hasReservations } currency requiresSession visualizationEnabled housekeepingEnabled customerSheetsEnabled externalBookingEnabled onboardingOnly waitlistEnabled slotBooking defaultDurationMinutes slotIntervalMinutes bookingWindowDays minNoticeMinutes openingHours { weekday startMinutes endMinutes } linkedCategoryIds } } ``` Returns all categories for admins and owners, or only assigned categories for staff, for the required UUID `clubId`, ordered by name then ID. `conversationRequired` controls whether reservations of that category expose a chat. Categories group alike resources and define shared modules: sessions, conversations, tickets, floor plans, housekeeping and customer sheets. `currency` is a shared lowercase three-letter billing code (the UI uses EUR). There is no pagination or additional filtering. Prices, optional security deposits and arrival defaults belong to resources. Nullable `depositAmount` is a fixed security-deposit amount in hundredths of a club credit, from 0 to 100,000,000, independent of the reservation price. New resources have no deposit. Each booking snapshots this amount (null becomes zero) and collects it in credits together with its price. Later resource edits do not change an existing booking’s deposit. `amount` is the price in hundredths of a club credit, from 0 to 100,000,000; zero means free. `priceUnit` (`ReservationPriceUnit`) says how the charged price (the default `amount` or the member's plan price) applies to a booking: `once` charges it a single time, `hour` charges it for every started hour of the booking, `day` charges it for every club-local calendar day the booking touches (a booking ending exactly at midnight does not count the following day), and `night` charges it for every club-local midnight crossed. Hour, day and night counts are at least one, and bookings without dates are charged one unit. Nullable `defaultCheckInMinutes` and `defaultCheckOutMinutes` are integer offsets before the start and after the end, from 0 to 10,080. Nullable `cancellationNoticeDays` is the cancellation notice, an integer from 0 to 365 days before the start; null or zero means members can always cancel with a refund. New resources start free and `once`-priced with no arrival defaults and no cancellation notice. Category edits preserve individual resource settings. `externalBookable` and nullable `externalAmount` expose a resource to non-members and set their price; see [external reservations](/en/graphql/external-reservations). ### Plan prices [#plan-prices] `planPrices` lists prices that replace `amount` for members of a given plan: `planId` is a subscription or add-on plan offered by the club and `amount` is in hundredths of a club credit, from 0 to 100,000,000. Zero makes the resource included in that plan; the member app shows any zero price as “Included in membership”. Staff receive every plan price; members always receive an empty list. When a signed-in member queries `reservationCategories` for their club, each resource’s `amount` is already their price: the lowest plan price among their unexpired subscription and add-on memberships, or the base `amount` when none of their plans prices the resource. The same rule prices member requests, staff bookings for that person, and resource changes on unpaid bookings. When a plan is split, its successor inherits the retained plan’s resource prices. ### Staff in charge of a resource [#staff-in-charge-of-a-resource] A resource can be linked to several staff members, such as the coaches giving a private lesson, and one staff member can be linked to several resources in any category of the club. `ReservationResource.staffUserIds` returns the linked staff users for staff callers and an empty list for members. `UpdateReservationResourceInput.staffUserIds` replaces the list: omit it to keep the current staff, pass an empty list to remove them all. Duplicates are ignored and at most 50 are accepted. Every staff member must have access to the resource's club; otherwise the result error is `Choose staff members of this club.` and nothing changes. A staff member who loses access to the club is unlinked from its resources automatically (see [admin and staff management](/en/graphql/auth#admin-and-staff-management)). A new list may not take away the staff a booking relies on: when a future, non-canceled booking on the resource was covered by the availability of at least one linked staff member (or the resource had no staff) and would no longer be covered, the result is `resource: null` with `error: "Some bookings on this resource would have no available staff member. Move or cancel them first."` and nothing changes, since booked session occurrences would otherwise disappear from the agenda. Bookings already outside the linked staff's availability do not block the change. Removing a staff account removes its links. Linking staff does not grant planning access: category assignments on the Team page still decide what staff can see. `clubStaff(clubId)` lists the staff members with access to the club (`id`, `name`, `email`), ordered by name then email, for the resource editor's Staff picker. It requires `catalog_manage`. A name equal to the email is returned as an empty string. ```graphql query ClubStaff($clubId: ID!) { clubStaff(clubId: $clubId) { id name email } } ``` Clicking any resource opens its editor. Without housekeeping it shows the resource fields directly, with no tab selector. Enabling housekeeping adds a separate Housekeeping tab with its checklist, state, minibar and history. Administrators and staff with `catalog_manage` can edit resource configuration; assigned staff can view it with `planning_read` and use housekeeping with `housekeeping_manage`. See [individual permissions](/en/graphql/auth#individual-staff-permissions). ```graphql mutation UpdateResource($input: UpdateReservationResourceInput!) { updateReservationResource(input: $input) { resource { id categoryId name amount priceUnit depositAmount defaultCheckInMinutes defaultCheckOutMinutes cancellationNoticeDays staffUserIds planPrices { planId amount } } error } } ``` The input requires UUID `clubId`, `categoryId`, `id`, a nonblank `name` of at most 100 characters without control characters, and integer `amount`. Omitting `priceUnit` preserves the current unit. Omitting `depositAmount` preserves the current deposit; explicit `null` clears it. Omitted or null arrival offsets clear the defaults. Omitting `cancellationNoticeDays` preserves the current notice; explicit `null` clears it. Omitting `planPrices` preserves the plan prices; a list replaces all of them, and an empty list removes them. Up to 100 plan prices are accepted, each plan at most once. Invalid values return `BAD_USER_INPUT`; plans the club does not offer return `resource: null` and an error without partial changes. The mutation is administrator-only and checks resource/category/club ownership. Duplicate names within a category or conflicting floor-plan library names return `resource: null` and an error without partial changes. Renaming preserves assignments and synchronizes reservation and floor-plan labels. Updating one resource does not affect its siblings or existing reservation prices and deposits. New reservations snapshot the selected resource’s price and use its check-in and check-out offsets; null defaults remain null. Staff may override arrival times. Category currency is snapshotted with the booking. Resource price edits apply to future bookings; existing booking charges remain unchanged. Changing an unpaid booking’s resource selection recalculates its price from the new resource. Changing the dates of a booking whose resources are priced per `day` or `night` recalculates the resource price for the new period, keeping product lines. Either change on a paid/completed booking or an existing checkout is rejected when it would change the stored charge. The total must not exceed 100,000,000 credit hundredths. `ReservationCategoryInput.resourceIds` optionally pairs each name in `resources` with its existing resource ID, or `null` for a new resource. The arrays must have the same length, with distinct IDs belonging to the edited category. Omitting an existing ID removes that resource subject to usage checks. Without `resourceIds`, names are reconciled as before. Creation only accepts null resource IDs. Renaming by ID preserves reservation assignments, blocked periods, housekeeping history, and floor-plan placements. It updates reservation display labels and linked library names and advances the saved plan revision when a placement label changes. Name swaps are atomic; a collision with another library object is rejected without partial changes. `reservationCategories.resources.hasReservations` reports whether any reservation (including historical or canceled reservations) protects a resource from deletion. The server checks again on save. Each category owns one or more named resources. Resource names are unique case-insensitively within each reservation category; different categories in the same club may use the same name. Admins add and remove resource names in the Resources section of the category editor; a resource already assigned to a reservation cannot be removed. When visualization is disabled, an unused resource can be removed even if the saved plan still references it. The same transaction removes its placements, advances the plan revision, and keeps any linked library artwork as a non-reservable object. An enabled plan still protects its linked resources. Both `createReservationCategory` and `updateReservationCategory` accept optional `newSessions` on `ReservationCategoryInput`. This is an append-only array of at most 50 `ReservationSessionDraftInput` objects, with the same schedule fields and validation as `createReservationSession`, but without `clubId` or `categoryId`: these are derived from the category being saved. Every session takes place on exactly one resource of its category. Because a category save can create its resources, a draft names its resource with the required `resourceName` instead of a `resourceId`: it must match one of the input's `resources` case-insensitively after trimming, otherwise the mutation returns `BAD_USER_INPUT`. The name is resolved after the category's resources are created or reconciled, so a draft can use a resource added or renamed in the same save; a draft naming a resource removed by that save returns `error: "Choose a resource of this category for each session."`. Standalone session creation and session editing take a required `resourceId`. A nonempty array requires `requiresSession: true`. Weekly drafts require `weekday` (0–6), `startMinutes` (0–1,439), and `durationMinutes` (1–10,080); custom drafts require strict UTC `startsAt` and `endsAt`, with the end after the start. Optional `capacity` accepts 1–10,000. Invalid drafts return `BAD_USER_INPUT` before persistence. Drafts follow the same one-session-per-resource rule as `createReservationSession`, against the category's existing sessions and the other drafts of the same save: an overlap returns `reservationCategory: null` and `error: "Another session uses this resource at that time."`, and a dated draft is also refused over a resource block or an active booking. The category and all new sessions are saved in one transaction; a failure rolls back the entire change. Existing sessions remain unchanged when editing or omitting the array. The mutation's return fields are unchanged; query `reservationAvailability` to read the sessions. ```graphql mutation CreateScheduledType($clubId: ID!) { createReservationCategory( input: { clubId: $clubId name: "Group lesson" description: "" conversationRequired: false requiresSession: true resources: ["Main studio"] newSessions: [ { kind: weekly resourceName: "Main studio" weekday: 1 startMinutes: 540 durationMinutes: 60 capacity: 8 } ] } ) { reservationCategory { id name requiresSession } error } } ``` This query returns only category metadata, never another member's reservations. Demo seeding supplies Hotel suite, Pool seating, and Private lesson for each demo club. Reservations are seeded only in the Ukab Clubs demo club, the only one with seeded members: every seeded reservation links to one of those members, whose name is its booking contact. That club gets four realistic reservations per category. Repeated seeding does not overwrite existing records. Startup also adds three reservations per category for the current UTC date, derived from the runtime clock rather than a hardcoded date. Their IDs are date-specific, so restarting on the same day does not duplicate or overwrite them; a later day's startup adds that day's bookings while preserving historical examples. `Group class` is seeded with `requiresSession: true` and one default session per club-local weekday at 15:00 in its `Main studio` resource, lasting 60 minutes and limited to eight members. ### Club timezone settings [#club-timezone-settings] Each club has a `timezone` (IANA identifier, default `Europe/Paris`). Staff with access to the club can read it with `clubSettings(clubId: ID!): Club`; only a club owner/admin can change it with `updateClubTimezone(clubId: ID!, timezone: String!): Club`. Both operations require access to the requested club; other callers receive `UNAUTHENTICATED`. The query returns the club, including `timezone` and `billingEnabled` (false when the platform turned the club's `billing` module off), or null if it does not exist; the mutation returns the updated club or null if it does not exist. `clubId` must be a UUID; `timezone` must be a supported IANA zone (or `UTC`), otherwise the mutation returns `BAD_USER_INPUT`. There are no filters, sorting or pagination. ```graphql query ClubSettings($clubId: ID!) { clubSettings(clubId: $clubId) { id name timezone } } mutation UpdateClubTimezone($clubId: ID!, $timezone: String!) { updateClubTimezone(clubId: $clubId, timezone: $timezone) { id timezone } } ``` ### Delete a reservation category [#delete-a-reservation-category] `deleteReservationCategory(clubId: ID!, id: ID!)` requires an owner/admin in the requested club. It returns `{ deleted: Boolean!, error: String }`. Invalid UUIDs return `BAD_USER_INPUT`; unauthorized, member, planning-only staff, and foreign-club calls return `UNAUTHENTICATED`. A missing category returns `deleted: false` with an unavailable error. Types with any existing reservations (including cancelled or completed ones) cannot be deleted, preserving booking history. A category used by an [onboarding booking step](/en/graphql/onboarding#booking-step), before or after approval, returns `error: "An onboarding booking step uses this category. Remove the step first."`. Otherwise the category, session definitions, blocked periods, and staff assignments are deleted together. The edit wizard requires confirmation before calling this mutation; failed deletion keeps the confirmation open with its error. ```graphql mutation DeleteReservationCategory($clubId: ID!, $id: ID!) { deleteReservationCategory(clubId: $clubId, id: $id) { deleted error } } ``` ### Read one category or resource [#read-one-category-or-resource] ```graphql query CatalogEntry($clubId: ID!, $categoryId: ID!, $resourceId: ID!) { reservationCategory(clubId: $clubId, id: $categoryId) { id name resources { id name } } reservationResource(clubId: $clubId, id: $resourceId) { id categoryId name amount } } ``` Both return null when the club has no such entry and require `catalog_manage`. They return the same fields as `reservationCategories`. ### Create and delete a resource [#create-and-delete-a-resource] A category update lists the resources to keep. Tools that manage each resource separately leave `resources` out of `ReservationCategoryInput` instead: a category created that way starts without resources, and updating it never adds, renames or deletes one. ```graphql mutation CreateReservationResource($input: CreateReservationResourceInput!) { createReservationResource(input: $input) { resource { id categoryId name amount } error } } mutation DeleteReservationResource($clubId: ID!, $categoryId: ID!, $id: ID!) { deleteReservationResource(clubId: $clubId, categoryId: $categoryId, id: $id) { deleted error } } ``` `CreateReservationResourceInput` takes `clubId`, `categoryId`, `name` and the settings of `UpdateReservationResourceInput` (`amount` defaults to 0), validated the same way. A name already used in the category, an unknown category, a plan price for a plan the club does not offer or staff from another club return `resource: null` with an `error`, and nothing is created. `deleteReservationResource` returns `deleted: false` with an `error` for an unknown resource, a resource holding any reservation, or one placed on the floor plan of a category with visualization enabled. Both require `catalog_manage`. ### Link categories on their own [#link-categories-on-their-own] ```graphql mutation SetReservationCategoryLinks($clubId: ID!, $categoryId: ID!, $linkedCategoryIds: [ID!]!) { setReservationCategoryLinks( clubId: $clubId categoryId: $categoryId linkedCategoryIds: $linkedCategoryIds ) { linkedCategoryIds error } } ``` Replaces the [linked categories](#moving-a-reservation-to-a-linked-category) of one category without sending the rest of it, which lets two categories link to each other: call it once per category. It takes at most 50 distinct ids of other categories of the club and requires `catalog_manage`. An unknown category, a link to itself or to a category of another club return `linkedCategoryIds: null` with an `error`. ### Sessions and blocked periods [#sessions-and-blocked-periods] `reservationAvailability(clubId, categoryId, from, to)` returns expanded session occurrences and the total `sessionCount` of configured session definitions for this club and category, independent of the requested date range or blocked periods. A count of zero triggers the back-office Manage sessions warning; it never prevents continuing the category wizard. It also returns blocked periods overlapping a strict UTC range of at most 62 days. Weekly definitions are expanded into dated occurrences; one-off custom sessions retain their stored date. Each session returns its opaque occurrence `id`, definition ID, non-null `resourceId`, dates, optional capacity, optional remaining places, `waitlistSize`, and the signed-in member's optional `myWaitlistEntryId` and `myWaitlistPosition`. Weekly `weekday` and `startMinutes` use the club's current wall timezone. A local time skipped by the spring DST transition produces no occurrence; an ambiguous time at the autumn transition uses the earlier instant. `durationMinutes` is elapsed time, so an occurrence can end at a different wall-clock offset. Changing the club's timezone affects future availability and booking resolution, not existing reservations. All returned `startsAt` and `endsAt` values remain UTC instants. Weekly IDs encode the club-local calendar date, including when the instant falls on another UTC date. Cancelled reservations do not consume capacity. Sessions in the past or overlapping a blocked period are not returned. ### Staff schedule [#staff-schedule] `staffSchedule(clubId, userId, from, page)` lets an administrator of the selected club read a coach's upcoming reservations and undated requests. `userId` is the staff account ID. It returns the same fields and uses the same pagination, ordering, resource assignments and date validation as `myStaffSchedule` below. Non-admin staff and member accounts cannot use this query, even to view their own schedule. Results are scoped to the selected club; canceled reservations and those ending on or before `from` are excluded. The admin interface exposes it under Team → staff dossier → Reservations, with a club selector for staff assigned to multiple clubs. ```graphql query CoachBookings($clubId: ID!, $userId: ID!, $from: String!, $page: Int!) { staffSchedule(clubId: $clubId, userId: $userId, from: $from, page: $page) { resources { id name categoryId categoryName } reservations { id categoryId guestName startsAt endsAt status } requests { id categoryId guestName proposedStartsAt status } hasMore } } ``` `myStaffSchedule(clubId, from, page)` returns the upcoming work of the signed-in staff member on every resource linked to them, across categories. Any staff member with access to the club may call it; it never returns other resources. `from` is a UTC ISO timestamp (the back office sends the start of the current club day) and `page` goes from 1 to 1000; other values return `BAD_USER_INPUT`. * `resources` lists the linked resources with `categoryId` and `categoryName`, ordered by category then name. * `reservations` lists non-canceled reservations ending after `from`, ordered by start, 30 per page. * `requests` lists undated `pending` or `booked` requests still waiting for a time. * `hasMore` is true when another page follows. An account with no linked resource in the club receives empty lists. The back office's My schedule page groups these reservations by day under the requests, with a Load more button, and links each one to its planning when the staff member can open that category. It refreshes every 30 seconds and when the window regains focus. ```graphql query MyStaffSchedule($clubId: ID!, $from: String!, $page: Int!) { myStaffSchedule(clubId: $clubId, from: $from, page: $page) { resources { id name categoryName } reservations { id guestName startsAt endsAt status } requests { id guestName } hasMore } } ``` ### Staff availability [#staff-availability] Each staff member has their own availability per club, independent of resources: * a **default week** of recurring windows (`weekday` 0 = Sunday, `startMinutes` and `endMinutes` in club wall time, end at most 1440 for midnight); * dated **exceptions**: `available` adds extra hours, `unavailable` takes time off. `staffAvailability(clubId, userId, from, to)` returns `windows`, the `exceptions` overlapping the range, and `periods`: the effective availability in the range, computed as the default week plus extra hours minus time off, clipped to the range. Weekly windows follow the club's timezone across daylight-saving changes. `configured` is false until the staff member has a default week or extra hours. Omit `userId` for the signed-in staff member; reading another staff member requires the administrator role in that club. The range follows the 62-day rule. ```graphql query StaffAvailability($clubId: ID!, $userId: ID, $from: String!, $to: String!) { staffAvailability(clubId: $clubId, userId: $userId, from: $from, to: $to) { configured windows { id weekday startMinutes endMinutes } exceptions { id kind startsAt endsAt } periods { startsAt endsAt } } } ``` Staff with `availability_manage` edit only their own availability: * `setMyAvailabilityWeek(clubId, windows)` replaces the default week. Overlapping or touching windows of a day are merged. At most 100 windows; invalid windows return `BAD_USER_INPUT`. The result holds the saved `windows`, or null `windows` and an `error`. * `addMyAvailabilityException(clubId, kind, startsAt, endsAt)` adds extra hours or time off (UTC timestamps, at most 62 days long). It replaces the part of any earlier exception it overlaps, so the latest dated change wins; the earlier exception keeps its remaining parts. * `removeMyAvailabilityException(clubId, id)` returns `removed: false` when the exception is not theirs. None of these changes may take away the availability a booking relies on. When a future, non-canceled booking on a resource linked to the staff member fits the effective availability of its linked staff before the change but would not afterwards, the change is not saved and the result carries `error: "Some bookings would fall outside your availability. Move or cancel them first."` (with null `windows`, null `exception` or `removed: false`). This keeps booked session occurrences on the agenda, since availability hides occurrences while none of their staff is available. Bookings that were already outside availability, for example on a resource whose staff was linked later, do not block unrelated changes. A booking stays covered while another linked staff member is available. ```graphql mutation SetMyAvailabilityWeek($clubId: ID!, $windows: [StaffAvailabilityWindowInput!]!) { setMyAvailabilityWeek(clubId: $clubId, windows: $windows) { windows { id weekday startMinutes endMinutes } error } } ``` When a resource has linked staff, every dated booking on it must fit entirely within the effective availability of at least one of them; a linked staff member without configured availability counts as available except during explicit time off. This applies to member requests, staff bookings, proposals, proposal acceptance and moving a booking to another time. Otherwise the result error is `The staff member is not available at this time.` (proposals return `This time is unavailable.`). Session-based bookings enforce the same staff availability check. Resource-scoped session occurrences are hidden from availability while none of their linked staff is available; a booking using a previously displayed occurrence is checked again before saving. Editing a booking without changing its time and resources without a linked staff member remain unrestricted. The back office's My availability page shows the staff member's week as a single grid: available hours in green and time off as dashed blocks. Dragging on empty time adds hours and dragging over green hours removes them; a dialog then asks whether the change applies every week (it edits the default week) or only on that date (it adds an exception). Clicking green hours opens the same choice for removing them, and clicking time off restores the usual hours. An Add hours button offers the same dialog with date and time fields. Administrators see a staff member's week read-only in the Availability tab of the Team page. Both refresh every 30 seconds and when the window regains focus. ### Session guest lists [#session-guest-lists] A session can optionally be restricted to a guest list. `ReservationSessionInput` accepts `guestPersonIds` (up to 500 people) and `guestPlanIds` (up to 50 plans). With both lists empty, which is the default, every member can see and book the session. Otherwise only listed people and members holding an unexpired membership on a listed plan in that club can. Both lists are deduplicated; malformed IDs or larger lists are rejected as `BAD_USER_INPUT`. Guests must be people of the session's club and plans must be offered by that club's merchant. `createReservationSession` and `updateReservationSession` otherwise return `error: "Choose guests from this club."` or `error: "Choose plans offered by this club."`. Omitting a list in `updateReservationSession` keeps the current one, including when a recurring occurrence is moved to a new one-off session; pass an empty list to remove a restriction. For members, `reservationAvailability` omits sessions they are not invited to. `createReservation`, `joinReservationWaitlist` and member rescheduling recheck the guest list under the session lock and return `error: "This session is reserved for its guest list."` for anyone else. Staff availability and staff bookings are not restricted. Session category drafts (`newSessions`) are always open; add a guest list afterwards from **Manage sessions**. ```graphql mutation GoldGala($input: ReservationSessionInput!) { createReservationSession(input: $input) { sessionId error } } ``` ```json { "input": { "clubId": "10000000-0000-4000-8000-000000000001", "categoryId": "73000000-0000-4000-8000-000000000004", "resourceId": "74000000-0000-4000-8000-000000000001", "title": "Gold gala", "kind": "custom", "startsAt": "2026-12-12T19:00:00Z", "endsAt": "2026-12-12T23:00:00Z", "guestPlanIds": ["60000000-0000-4000-8000-000000000102"], "guestPersonIds": ["00000000-0000-4000-8000-000000000001"] } } ``` When a capacity-limited occurrence is full and the category's `waitlistEnabled` is true (the default), an authenticated member can call `joinReservationWaitlist(input)` with its occurrence ID. The entry always uses the session's resource; `resourceId` can be omitted and, when given, must be that resource. With `waitlistEnabled` false the mutation returns `error: "This reservation category has no waitlist."`. Time slot waitlists are described in [Booking rules and time slots](#booking-rules-and-time-slots). The server rechecks active membership, occurrence scope, capacity, resources and blocks under the session lock, then returns a FIFO `ReservationWaitlistEntry` in `ReservationWaitlistResult.waitlistEntry` (`id`, `clubId`, `categoryId`, `sessionOccurrenceId`, `startsAt`, `endsAt`, `guests` and `position`; `sessionOccurrenceId` is null for a time slot waitlist), or `waitlistEntry: null` with the refusal in `error`. Joining the same occurrence again is idempotent and returns the existing entry and current position. `leaveMyReservationWaitlist` removes only the signed-in member's entry. When a place opens, a background worker books waiting members in FIFO order through the normal member reservation checks, removes their entry and emits `waitlist.joined`, which notifies and emails them. An entry whose booking is refused stays in the queue without blocking later members. Each pass scans entries in bounded pages, including entries beyond a page of refused bookings; entries joining during the pass are handled on the next pass. Admins manage availability with `createReservationSession`, `deleteReservationSession`, `createReservationBlock`, and `deleteReservationBlock`. A session input is either `weekly`, with a club-local weekday (0 Sunday through 6 Saturday), start minute (0–1,439), and duration; `daily`, with the same time/duration and no weekday; or `custom`, with strict UTC start/end timestamps. Every session input requires the `resourceId` of one resource of its category. Capacity is optional and accepts 1–10,000 members. `createReservationSession` returns a `CreateReservationSessionPayload` with the new `sessionId` or an `error`, and creates nothing on error. A resource hosts one session at a time: a session overlapping a future, uncanceled occurrence of another session on the same resource returns `error: "Another session uses this resource at that time."`, whether either one is dated or recurring (recurring sessions are compared over all their future occurrences). Like an edited session, a new dated session is also refused over a resource block (`"The resource is blocked during this session."`) or an active booking on that resource (`"The resource is already reserved during this session."`). A recurring session only checks other sessions; blocks and bookings hide its individual occurrences instead. Flexible reservation categories use blocked periods, for example while a hotel suite is under renovation. The back office exposes the matching **Manage sessions** or **Block periods** action on each category. `deleteReservationSession` returns a `DeleteReservationSessionResult` with `deleted` and an `error`. It deletes nothing and returns `error: "This session has bookings or a waitlist. Cancel the session to notify its members."` while the session has a pending, booked or confirmed reservation, an unrefunded payment or a waitlist entry; cancel the occurrence with `cancelReservationSession` instead. An unknown session returns `error: "Session not found."`. A block input carries an optional `resourceId`. Omitted or null blocks every resource of the category; a resource ID limits the block to that resource, for example closing the pool while its deck chairs stay bookable. The resource must belong to the category, otherwise the mutation returns `error: "Choose a resource from this reservation category."`. Each returned block exposes its `resourceId`, null for category-wide blocks. Blocked periods reject overlapping pending or confirmed reservations that use a blocked resource; those reservations must be cancelled first, while reservations on other resources do not prevent a resource-scoped block. Customer and staff reservation creation, member rescheduling and proposal acceptance all reject a block that is category-wide or that covers one of the chosen resources. Only category-wide blocks hide session occurrences from `reservationAvailability`. The calendar shows a category-wide block on every resource row and a resource-scoped block on its own row only, so other resources keep their add action. The **Block periods** dialog has an **Applies to** choice that defaults to all resources; the add dialog opened from a resource's plus action has a **Block period** tab whose choice defaults to that resource. Calendar events keep their exact times across midnight instead of moving multi-day reservations into a separate all-day band. Clicking a blocked event opens the removal confirmation, which names the resource for a resource-scoped block. ## Reservations [#reservations] ### Participant and waitlist export [#participant-and-waitlist-export] The staff session panel switches between **Participants** and **Waitlist**. Each view offers a CSV download for that occurrence using the same authorized, unpaginated data as its table. It contains one row per reservation with the person's name, reservation status, attendance, reserved places (including the booking contact), session name and start/end instants. Labels use the interface language; timestamps use ISO 8601 UTC, with explicitly labeled UTC columns. Filenames include a sanitized session name and its UTC date/time. `ReservationRosterEntry.guests` exposes the total number of reserved places in both roster queries and attendance mutation results. Pending and canceled reservations are excluded. The export is disabled while loading, refreshing, saving attendance, on query errors, or when the roster is empty. The UTF-8 CSV includes a BOM, semicolon separators, quoted cells and spreadsheet formula protection for names. Staff with `reservations_manage` can remove a participant from the roster. After confirmation, the web app cancels that reservation with `cancelReservation`, so the usual staff cancellation refunds apply; completed participants cannot be removed. Each roster entry's nullable `refundDeadline` is the session's [cancellation notice](#cancellation-notice) deadline for a paid `confirmed` booking, and null when nothing was collected. Once it has passed, the removal dialog asks whether to refund the price and passes the answer as `refundLateCancellation`. The reservation detail asks the same only for a paid `confirmed` reservation. The waitlist export contains FIFO position, person, selected resource, join time, session name, and session start/end instants. Its export is likewise disabled while unavailable or empty. ### Confirmation emails [#confirmation-emails] Creating a confirmed reservation for a member, confirming a pending request, or accepting an alternative time for a free reservation queues a confirmation email. Paid reservations queue the email only after payment confirms them; repeated payment notifications do not send duplicate confirmations. Pending requests and edits that keep an already confirmed status do not send another confirmation. Cancelling and confirming again creates a new confirmation; an unsent previous confirmation is discarded. Existing reservations and demo seeds are not emailed retroactively. The email links to the consumer's `/account/reservations/<id>?club=<clubId>` detail page. The email includes the club, reservation category, resource, dates and a link to the reservation in the member account. Dates use the club's configured timezone. The link requires the member to sign in; it does not contain an authentication token. Delivery runs in the background with the configured mail provider and `CONSUMER_ORIGIN`. The durable `reservation_mail` queue commits with the reservation change. The worker checks for work every minute, skips cancelled or ended reservations and missing members, and retries definite delivery failures after one hour. An uncertain provider outcome or an interrupted sending attempt is retained as `unknown` and is not automatically resent, to avoid duplicate emails. Provider acceptance does not guarantee inbox delivery. For local testing, create a future confirmed reservation for a test member, then open Mailpit after at most one minute. Confirming a pending request should also create one email; saving the confirmed reservation again should not. Email links use the consumer origin configured for the running stack. ### QR tickets [#qr-tickets] The category modal's **Create QR code ticket** switch sets `ReservationCategoryInput.createTicket` (optional Boolean, defaults to false). Type queries return `createTicket`; each reservation also returns its effective `createTicket` flag. Type changes propagate to existing reservations. Tickets are private, cover the whole reservation, and are available only for confirmed, member-owned reservations with tickets enabled and an end time strictly in the future. The booking contact and every accepted invitee download the same ticket with the same code. ```graphql mutation DownloadMyReservationTicket($clubId: ID!, $id: ID!) { downloadMyReservationTicket(clubId: $clubId, id: $id) { filename pdf error } } mutation VerifyReservationTicket($clubId: ID!, $code: String!) { verifyReservationTicket(clubId: $clubId, code: $code) { valid message categoryName attendance guests { id name status } reservation { id categoryId guestName guestAvatar startsAt endsAt guests resource { id name } } } } ``` Download requires the session of the owning member or of an accepted invitee; the server derives the person ID from that session. `id` is the reservation ID. Pending or declined invitees receive the same unavailable error as unrelated members. `filename` and `pdf` (base64-encoded PDF bytes) are nullable and are returned only on success, with `error: null`. Unavailable reservations and generation/storage failures instead return a safe `error` with null file fields. UUID arguments are validated; no filtering, sorting or pagination applies to these mutations. Verification requires staff access to the selected club. The scanner is available from the reservations/planning toolbar; no reservation ID is needed. The server resolves the code within that club. `code` is a six-digit number represented as a string, including leading zeros. Club administrators can verify all reservation categories in their club; planning staff can verify only their assigned reservation categories. The QR contains only `xxx-xxx`; manual verification accepts `xxx-xxx` or `xxxxxx`, with surrounding whitespace ignored. Invalid, expired, cancelled, disabled or unknown or out-of-scope tickets return `valid: false` with null `reservation`, `categoryName` and `attendance` and an empty `guests` list. A valid ticket returns the matching `Reservation` (whose `guestName` is the booking contact), its category name, current attendance, and `guests`: the reservation's `ReservationInvitee` entries with status `accepted` or `pending`, ordered by invitation time. Declined invitees are omitted. Verification never checks in guests or consumes a ticket. It never changes attendance; a successful scan must be followed by an explicit `markReservationAttendance` call if staff wants to record presence. ### Session attendance [#session-attendance] ```graphql query SessionRoster($clubId: ID!, $categoryId: ID!, $occurrenceId: ID!) { reservationRoster(clubId: $clubId, categoryId: $categoryId, occurrenceId: $occurrenceId) { reservationId personId personName status attendance refundDeadline } } query SessionWaitlist($clubId: ID!, $categoryId: ID!, $occurrenceId: ID!) { reservationWaitlist(clubId: $clubId, categoryId: $categoryId, occurrenceId: $occurrenceId) { id personId personName position joinedAt resource { id name } } } mutation MarkAttendance( $clubId: ID! $categoryId: ID! $occurrenceId: ID! $reservationId: ID! $attendance: ReservationAttendance! ) { markReservationAttendance( clubId: $clubId categoryId: $categoryId occurrenceId: $occurrenceId reservationId: $reservationId attendance: $attendance ) { reservationId personName attendance } } ``` Use the opaque `id` returned by `reservationAvailability.sessions` as `occurrenceId`: custom sessions use their definition UUID; weekly sessions use `<definition UUID>:<YYYY-MM-DD>` in the club's local timezone. All club, category and reservation IDs must be UUIDs. Owners/admins can access their club's roster; planning staff must additionally be assigned to the selected reservation category. Members, anonymous users, foreign clubs and unassigned staff are denied. The roster has no pagination, filters or client sorting; it contains `booked`, `confirmed` and `completed` reservations for that exact occurrence, ordered by booking contact name and reservation ID. Pending and canceled bookings are excluded. `personName` is the linked person's current first and last name, falling back to the booking contact when the person is no longer linked. `personId` may be null. Ticket codes are never returned. Attendance is stored separately from the check-in window (`checkInAt`), defaults to `unmarked`, and may be set to `present`, `late`, `absent` or `unmarked`. Marking returns null if the occurrence or eligible booking does not match the selected club/category; the roster returns an empty list for an unknown occurrence. A ticket verification does not mark attendance, including on success. `reservationWaitlist` uses the same occurrence and staff authorization rules. It returns active entries in FIFO order with one-based positions, the selected resource, and ISO 8601 join times. An unknown occurrence returns an empty list. Waitlisted people are not attendance records. Codes are issued once, transactionally, on first download, and remain unchanged after edits, cancellation or category configuration changes. Issuance serializes per club; a database unique index prevents two reservations in the same club sharing a code, and concurrent downloads share the same persisted code. Codes are not reused while the reservation remains stored. The six-digit namespace has a maximum of one million issued reservations per club; exhaustion returns an issuance error rather than reusing another reservation's code. Current status, expiry and ticket-enabled configuration always determine validity. Database-backed limits allow at most 100 verification attempts per club in a one-minute window, counting both valid and invalid submissions. Limits apply across staff sessions and API instances and survive restarts; excess attempts return `valid: false` with a retry message. PDF details refresh independently of the code. Content-derived storage keys include names, dates, guest counts, club name and timezone. The server rechecks eligibility and content after storage I/O. The centered PDF shows the stable code, natural-language dates in the club timezone and additional guests (`guests - 1`, excluding the main person), with no exposed UUIDs or payload identifiers. The back office ticket dialog switches between a camera scanner (cross-browser decoding, permission-error retry) and manual code entry. A valid result shows the guest, category, period, resources, guests and attendance, with a **Mark present** action that calls `markReservationAttendance`. Closing the scanner releases its camera and decoder. When tickets are disabled, the entire verifier is absent. #### Storage configuration [#storage-configuration] Configured S3 storage uses `TICKET_S3_BUCKET`, `TICKET_S3_ENDPOINT`, `TICKET_S3_ACCESS_KEY_ID`, `TICKET_S3_SECRET_ACCESS_KEY`, and optional `TICKET_S3_REGION` (default `fr-par`). Objects use private ACLs and content-specific keys under `reservation-tickets/`. Credentials belong in the runtime's existing secret configuration, never in frontend bundles or another worktree's files. For isolated development only (`NODE_ENV=development`), when no S3 settings are supplied, the adapter persists private files under `TICKET_STORAGE_DIRECTORY`, defaulting to `/app/.local/reservation-tickets`. The Compose stack sets it to the `ticket_storage` named volume at `/var/lib/champagne/reservation-tickets`, keeping container-owned files out of the worktree. Writes are atomic, survive API restarts, and never fall back to ephemeral storage in production or when an S3 configuration is incomplete. Production requires a configured private S3 provider. ```graphql query Reservations( $clubId: ID! $categoryId: ID! $page: Int $pageSize: Int $from: String $to: String $undated: Boolean ) { reservations( clubId: $clubId categoryId: $categoryId page: $page pageSize: $pageSize from: $from to: $to undated: $undated ) { items { id clubId categoryId personId guestName resource { id name } startsAt endsAt checkInAt checkOutAt proposedStartsAt proposedEndsAt proposedFree proposalVersion proposedAmount paymentVersion guests amount currency status } total page pageSize } } ``` Both IDs are required UUIDs. Results match **both** club and category; an unknown category or a category from another club returns an empty page, never another club's records. `page` defaults to 1 (range 1–1,000,000); `pageSize` defaults to 25 (range 1–100). Explicit `null` uses the same defaults. Invalid values receive `BAD_USER_INPUT`. Results are ordered by start time descending, then ID ascending, with no client-selectable sorting. `total` counts all matching rows, including date filters; out-of-range pages have an empty `items` array and retain the matching total. Optional `from` and `to` must be supplied together as UTC ISO timestamps, for example `2026-09-01T00:00:00.000Z` and `2026-10-01T00:00:00Z`. Seconds are required; exactly three fractional digits are optional. The range must increase and span at most 62 days. Missing endpoints, invalid dates, offsets other than `Z`, and invalid durations receive `BAD_USER_INPUT`. Omitted or both-null endpoints leave the list unbounded by date. The range uses overlap semantics: `startsAt < to AND endsAt > from`. A stay that began before the range is included if it continues into it. Bookings ending exactly at `from` or starting exactly at `to` are excluded. Undated requests never match a date range. `undated` defaults to `false`. When `true`, only pending requests without a time (see [Request a reservation](#request-a-reservation)) are returned; the back office lists them as **Requests awaiting a time** above the calendar of conversation-enabled categories. ### Member's upcoming reservations [#members-upcoming-reservations] ```graphql query MyReservations($clubId: ID!) { myReservations(clubId: $clubId) { categoryName conversationRequired hostedSlug reservation { id resource { id name } startsAt endsAt checkInAt checkOutAt proposedStartsAt proposedEndsAt proposedFree proposalVersion proposedAmount paymentVersion guests status phase timingChangesAt } editable } } query MyReservationWaitlist($clubId: ID!) { myReservationWaitlist(clubId: $clubId) { id clubId categoryId categoryName sessionOccurrenceId startsAt endsAt joinedAt guests position resource { id name } } } ``` Only a member session belonging to `clubId` can call this query. It returns that member's pending, booked, and confirmed reservations whose end is still in the future or that do not have a time yet, ordered by start time (undated requests last) then ID. It never accepts a person ID and never returns another customer's bookings. `hostedSlug` is nullable; it remains available for other club-hosted experiences, but it does not imply a reservation chat route. Member apps use `conversationRequired` to decide whether to expose their own conversation UI. `editable` on `CustomerReservation` (also on `myReservation`) tells whether `updateMyReservation` would currently accept a change of dates, resource or people: a dated `pending` request, a dated `booked` reservation outside conversation-enabled categories, or a `confirmed` one before its cancellation notice (`refundDeadline`), always before the reservation starts: once started, `editable` is false and `updateMyReservation` fails with `This reservation has already started. Contact the club.` It is computed when read, so apps never re-derive the notice policy. `myReservationWaitlist` has the same member-only club scope and returns the member's future waitlist entries with their current FIFO positions and party size (`guests`). `resource` is the session's resource, or null for a time slot waitlist, like `sessionOccurrenceId`. The consumer combines them with bookings in the upcoming area and lets the member leave directly from the entry. The back office offers Day, Week and Month views (Month by default), with a Monday-first week and six weeks in the month grid. Previous/next navigation moves by the selected view, and Today returns to the current date. It converts local midnight boundaries to UTC, including daylight-saving transitions, and fetches **every** matching page at 100 rows per request. It does not render a partial first page as a full calendar. Query caches include club, category and date range. A later-page failure shows an error and retains a previously completed cached result, if available. Multi-day reservations appear on every overlapping local date and in a separate multi-day band above the day/week time grid. Timed events occupy proportional quarter-hour slots with separate lanes for overlaps; their exact times remain visible in event details. Click an event to view read-only details. Month cells show two events and an overflow button that opens that date's Day view. Week grids scroll horizontally on small screens. Weekends are shaded and today is highlighted. There is no table view. Click the month/year heading to open a modal containing a standalone HeroUI Calendar (with year navigation). Selecting a date navigates the current agenda view and closes the modal; there is no date input field. The UI uses an Agenda-style composition of existing HeroUI primitives and standard layout utilities, not the separately licensed HeroUI Pro package. `guestName` is the booking contact (renaming the linked person with `updatePerson` or `updateMyProfile` renames it on all their reservations in the club), `personId` is the linked customer when retained, and `resource` is the suite/facility/lesson, `startsAt` and `endsAt` are ISO 8601 UTC timestamps, both null only for a pending or canceled request whose time the club has not proposed yet, and `guests` is the number of people, an integer from 1 to 500 including the booking contact (see [number of people](#number-of-people)). `amount` and `currency` snapshot the total resource price and category currency when the reservation is created, so later category edits do not alter an existing booking. `status` is `pending`, `booked`, `confirmed`, `completed`, or `canceled`. `source` (`ReservationSource`) records where the reservation was created and never changes: `staff` when booked by staff in the back office (`createStaffReservation`, including session participants), `member` for the member area (`createReservation` and waitlist promotions), and `external` for a non-member's [external reservation](/en/graphql/external-reservations). The back-office detail shows it in a **Reservation source** card. The web table displays times in the viewer's local timezone. The database enforces a positive duration and a category belonging to the same club. Nullable `checkInAt` and `checkOutAt` must respectively be at or before the start and at or after the end. Nullable `checkedOutAt` is when staff recorded the guest leaving early (see [Early check-out](#early-check-out)). `proposedStartsAt` and `proposedEndsAt` are either both null or a valid alternative window; the requested window stays unchanged until the member accepts the proposal. `proposedFree` is `true` when staff offered the proposed session for free; it is always `false` without a proposal. ### Computed reservation fields [#computed-reservation-fields] The API decides these rules when the fields are read, so the back office and the member apps display them instead of re-deriving them: | Field | Meaning | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `requiresPayment: Boolean!` | `amount + depositAmount > 0`. Accepting a pending request then books it (`booked`, payment due) instead of confirming it. | | `phase: ReservationPhase` | For a dated `confirmed` reservation: `checkIn` from `checkInAt` (or the start) to the start, `ongoing` until the end, `checkOut` until `checkOutAt` (or the end), and `checkedOut` once staff recorded an [early check-out](#early-check-out). Null otherwise. | | `canCheckOut: Boolean!` | Staff can record the guest leaving now (see [Early check-out](#early-check-out)). | | `canUndoCheckOut: Boolean!` | Staff can clear the recorded departure now. | | `depositSettleable: Boolean!` | The reservation is `confirmed`, its deposit was paid and its departure has passed, so staff can [refund or keep the deposit](#departure-and-deposit-refunds). | | `timingChangesAt: String` | The next instant, as an ISO 8601 UTC timestamp, when `phase`, `canCheckOut`, `canUndoCheckOut` or `depositSettleable` change; null when they no longer change. | Because these depend on the current time, clients keep only a timer: the web reservation page and staff schedule, and the member reservation list and detail, refetch at `timingChangesAt`. ```graphql query ReservationDecisions($clubId: ID!, $id: ID!) { reservation(clubId: $clubId, id: $id) { id requiresPayment phase canCheckOut canUndoCheckOut depositSettleable timingChangesAt } } ``` ## Create a category [#create-a-category] ```graphql mutation CreateReservationCategory($input: ReservationCategoryInput!) { createReservationCategory(input: $input) { reservationCategory { id clubId name description conversationRequired resources { id name amount depositAmount defaultCheckInMinutes defaultCheckOutMinutes } currency } error } } ``` `input` includes `clubId`, `name`, `description`, `conversationRequired`, `currency`, `resources`, and module switches. Names must be nonblank, at most 100 characters before trimming, and contain no ASCII control characters. Invalid names/IDs receive `BAD_USER_INPUT`. Names are trimmed before persistence and unique case-insensitively within each club. Duplicate names return `reservationCategory: null` with a user-facing `error`; success returns the new category with `error: null`. The database uniqueness index also protects concurrent requests. Unexpected failures are masked by GraphQL. `updateReservationCategory(id, input)` accepts the same configuration and club scope. `housekeepingEnabled` defaults to `false` and turns on the [housekeeping module](#housekeeping) for the category. The back-office wizard sets it in **Options** using the **Housekeeping module** switch. Checklist configuration belongs to individual resources, not this wizard. Disabling it hides the module and refuses new entries without deleting logged tasks or reports. `customerSheetsEnabled` defaults to `false` and turns on [customer sheets](#customer-sheets) for the category, using the **Customer sheets** switch in **Options**. Disabling it hides existing sheets and refuses edits without deleting them; enabling it again restores them. `externalBookingEnabled` defaults to `false` and opens the category to [external reservations](/en/graphql/external-reservations) through the **External booking** switch. It cannot be combined with `conversationRequired` or `requiresSession`. `onboardingOnly` defaults to `false`. When true, the category is only booked through an [onboarding booking step](/en/graphql/onboarding#booking-step), set with the **Book only through onboarding** switch in **Options**. Members do not see it: `reservationCategories` leaves it out, `reservationSlots` returns `null` for it, it is never offered as an alternative to a full slot, and `createReservation` returns `error: "This reservation category is not available in your club."`. Staff still list it and see its reservations in planning like any other. It cannot be combined with `externalBookingEnabled`; sending both receives `BAD_USER_INPUT`. ### Number of people [#number-of-people] Each resource has a `maxOccupants`, 1 by default (see [resource occupant limits](#resource-occupant-limits)). `guests` on `CreateReservationInput`, `StaffReservationInput`, `UpdateReservationInput` and `UpdateMyReservationInput` is the number of people coming, from 1 to 500 including the booking contact; other values receive `BAD_USER_INPUT`. When the selected resource takes more than one person, the member booking and edit forms and the staff editor ask for a **Number of people**; otherwise the field is hidden and the booking is for one person. * Members cannot exceed the `maxOccupants` of the selected resource: creation returns `error: "Too many people for this resource."`, and member edits and `myReservationChangeQuote` return the same error. The member forms show the limit and disable booking above it. * Staff can exceed `maxOccupants`, for example to seat 10 people at a table for 4. The editor and the staff reservation page show a warning but do not block the save. * In session categories, the number of people counts against session capacity. * The price does not depend on the number of people. * [External bookings](/en/graphql/external-reservations) follow the member rules. [Inviting people](#people-invited-to-a-reservation) is optional and gives them access to the reservation. Members are offered invitations only when `guests` is above 1: the booking contact plus pending and accepted invitees cannot exceed `guests`, and a further member invitation fails with `Everyone in this booking has already been invited.` Staff always see the people list: when the reservation is full, the staff page warns that inviting adds a person, and a staff invitation raises `guests` by one (recorded in the reservation history) instead of failing. For a session booking, that extra person must still fit in the session: a staff invitation that would take the session past its capacity fails with `This session does not have enough places left.` Lowering `guests` below the current invitees returns `error: "The number of people cannot be lower than the people already invited."`. ## Booking rules and time slots [#booking-rules-and-time-slots] Each category carries booking rules that members and non-members must follow. Staff can always book outside them. | Field | Values | Effect | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `openingHours` | Up to 50 weekly windows (`weekday` 0 Sunday–6 Saturday, `startMinutes` 0–1,439, `endMinutes` up to 1,440) in club wall time | A booking must fit inside one window. Overlapping windows of a day are merged. An empty list means always open. | | `defaultDurationMinutes` | Null or 5–10,080 | When set, members and non-members cannot choose the end: it is the start plus this length, whatever `endsAt` they send. The staff editor prefills it for new bookings. | | `bookingWindowDays` | Null or 1–730 | Members cannot book a start further ahead. | | `minNoticeMinutes` | Null or 0–10,080 | Members cannot book a start sooner. | | `slotBooking` | Boolean | Members choose a party size and a start time instead of a resource (see below). Requires `defaultDurationMinutes` and cannot be combined with `requiresSession`, `conversationRequired` or `externalBookingEnabled`. | | `slotIntervalMinutes` | 5, 10, 15, 20, 30 or 60 (default 30) | Spacing of the start times offered in slot booking. | | `waitlistEnabled` | Boolean, default true | Members can join a waitlist for a full session or time slot. | | `linkedCategoryIds` | Up to 50 other categories of the club | Staff can move reservations to these categories, and members are offered them when a time slot is full. Links are one-way. | `ReservationCategoryInput` accepts the same fields. On update, omitting `openingHours` or `linkedCategoryIds` keeps them; any list replaces them. The other fields fall back to their defaults when omitted. Out-of-range values, invalid windows, unknown ID formats and invalid slot booking combinations receive `BAD_USER_INPUT`; linking the category to itself or to another club returns `error: "Choose other reservation categories of this club."`. Refusals returned by `createReservation`, `updateMyReservation` and [`createExternalReservation`](/en/graphql/external-reservations): * `The club is closed at this time.` when the booking does not fit in the opening hours. * `This time is too soon to book online. Contact the club.` before the minimum notice. * `This time is not open for booking yet.` beyond the booking window. Session categories only apply the booking window; sessions define their own times. ### Slot booking [#slot-booking] A restaurant is a slot-booking category whose tables are resources with their `maxOccupants`. Members send no resource: `createReservation` omits `resourceId` and takes `startsAt` and `guests`, and the server assigns the smallest free resource that holds the party (then by name), under the category lock so concurrent bookings cannot take the same table. When none is free the mutation returns `error: "Nothing is available at this time for this party."`. `updateMyReservation` keeps the current table when it still fits and is free, otherwise it assigns another one. Staff still book and move specific tables from the calendar. ```graphql query ReservationSlots($clubId: ID!, $categoryId: ID!, $date: String!, $guests: Int!) { reservationSlots(clubId: $clubId, categoryId: $categoryId, date: $date, guests: $guests) { partyFits waitlistEnabled slots { startsAt endsAt available alternativeCategoryIds waitlistEntryId } } } ``` `date` is a club-local `YYYY-MM-DD` day and `guests` 1 to 500; other values receive `BAD_USER_INPUT`. The query returns null when the category does not use slot booking. Start times run every `slotIntervalMinutes` through each opening window, stopping when the booking length no longer fits, and exclude past times and times outside the booking window. `partyFits` is false, with no slots, when no resource holds the party. `available` is true when a fitting resource is free and not blocked for the whole slot. For a full slot, `alternativeCategoryIds` lists linked slot-booking categories where the same start time is free for the party. `waitlistEntryId` is the signed-in member's waitlist entry for that time. Members of the club and staff with planning access can query it. ### Time slot waitlist [#time-slot-waitlist] When a slot is full, members call `joinReservationWaitlist` without `resourceId`, with `startsAt` and `guests` instead of a session occurrence: ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "categoryId": "6a000000-0000-4000-8000-000000000001", "startsAt": "2026-10-02T17:30:00.000Z", "guests": 4 } } ``` The time must be one of the category's slots. The entry is refused with `A place is available. Book it instead.` while a fitting resource is free, with `You already booked this time.` when the member has a booking at that time, and with `Too many people for this resource.` when no resource holds the party. Joining again returns the existing entry. Returned entries have a null `sessionOccurrenceId` and carry `guests`. The waitlist worker books waiting parties in joining order when a table frees up, using the normal member rules, and emits `waitlist.joined`. Staff list upcoming entries with: ```graphql query ReservationSlotWaitlist($clubId: ID!, $categoryId: ID!, $from: String!, $to: String!) { reservationSlotWaitlist(clubId: $clubId, categoryId: $categoryId, from: $from, to: $to) { id personName startsAt guests position joinedAt } } ``` The range spans at most 62 days. Entries are ordered by start time, then position. The back office shows them from the category's **Waitlist** button. ### Moving a reservation to a linked category [#moving-a-reservation-to-a-linked-category] `UpdateReservationInput.categoryId` moves a reservation to a category linked from its current one, for example from a restaurant to its rooftop. `resourceId` must then belong to the target category. Moving to an unlinked category returns `error: "This reservation cannot be moved to that category."`. Session reservations, session categories and categories in another currency return `error: "Only reservations without sessions can move to a category with the same currency."`. The staff reservation editor offers the current and linked categories. ### Category settings [#category-settings] **Category settings** opens a dialog with tabs, laid out like the club settings. Edits made in any tab are saved together with the dialog's **Save** button, and **Delete category** sits in its footer. * **General**: name. * **Resources**: the resource list with each resource's capacity, a button opening its settings drawer, and the floor plan switch. * **Booking**: **How members book** is one choice between a resource and a time, a party size and a time (`slotBooking`), a session (`requiresSession`) or a request to discuss (`conversationRequired`), with the waitlist and non-member switches that apply to it. Then the booking length and start times, the booking window, and linked categories. * **Hours**: opening hours. * **On site**: QR tickets, housekeeping and customer sheets. The **Add category** wizard offers presets (Restaurant, Group class, Treatment, Room or suite) that fill these settings once; nothing records which preset was used and every setting stays editable. ## Request a reservation [#request-a-reservation] ```graphql mutation CreateReservation($input: CreateReservationInput!) { createReservation(input: $input) { reservation { id clubId categoryId guestName resource { id name } startsAt endsAt guests status } error } } ``` Example variables (replace IDs with your club and one of its reservation categories): ```json { "input": { "clubId": "51000000-0000-4000-8000-000000000051", "categoryId": "73000000-0000-4000-8000-000000000001", "resource": "Garden suite", "startsAt": "2090-09-21T15:00:00.000Z", "endsAt": "2090-09-22T11:00:00.000Z", "guests": 2 } } ``` `startsAt` and `endsAt` are optional, but must be supplied together. They may be omitted (or null) only for conversation-enabled categories without sessions: the request is then created as `pending` with null dates, and staff propose its time. Other flexible categories without both dates return `error: "Choose a start and end time."`. `sessionOccurrenceId` is optional for flexible categories and required when the selected category has `requiresSession: true`. Only a **member session** for `clubId` can call this mutation; staff cannot create requests on behalf of members. The person ID comes from the session and the booking contact name comes from the database. Neither identity nor status can be supplied by the client. The persisted request retains its member ID and a snapshot of the contact name. The account must still be active and belong to this club. Activity uses the effective account status on today’s UTC date: future and elapsed suspensions allow requests; suspensions block them from their start date through their end date, inclusive. Banned and disabled accounts remain blocked. It must have a current subscription membership (or legacy membership without a plan), scoped to the club or legacy unscoped, with no expiration or an expiration on/after today's UTC date. An add-on alone, an expired membership, and a membership explicitly scoped to another club do not qualify. Membership eligibility is checked at request time, not against the requested stay dates. The category must exist in the same club. These eligibility failures return `reservation: null` and an actionable `error`. Session-based bookings must select a future occurrence from that category; the server replaces the submitted dates with the authoritative session dates and books the session's resource. `resourceId` can be omitted for a session; when given, it must be the session's resource, otherwise the result error is `Choose the resource assigned to this session.` Capacity is serialized transactionally and counts the `guests` of pending and confirmed reservations. Flexible bookings are rejected when their interval overlaps an admin-defined blocked period. IDs must be UUIDs. Every reservation has exactly one resource: inputs take a single `resourceId` from the selected reservation type, and outputs return a non-null `resource`. `updateReservation` requires it. `createStaffReservation` and `createReservation` require it except for session bookings, which always use the session's resource, and, for `createReservation`, in slot-booking categories, where it is omitted and the server assigns a table. `updateMyReservation` requires it except for session bookings, which keep the session's resource, and in slot-booking categories. Session waitlist joins may omit it; time-slot waitlist joins omit it. A missing required `resourceId` or a malformed ID receives `BAD_USER_INPUT`. `guests` must be an integer from 1 to 500; see [number of people](#number-of-people). Timestamps use the same strict UTC format as date filters. The end must be strictly later than the start. Past dates and reservations longer than 62 days are accepted; booking duration and distance into the future are not capped. Invalid inputs receive `BAD_USER_INPUT`; unexpected failures remain masked. Success returns the persisted reservation with UTC timestamps and `error: null`. With chat disabled (`conversationRequired: false`), an available resource is booked immediately: paid reservations start as `booked` and free reservations as `confirmed`. With chat enabled, reservations remain `pending` until the club workflow approves them. Sessions and blocked periods and overlapping resource reservations constrain availability; external provider inventory is not synchronized. Paid bookings must be paid in club credits before confirmation. Repeated API calls create separate requests; the consumer disables submission while pending and after success and does not automatically retry writes. After an ambiguous network failure, contact the club before resubmitting. The consumer account page loads categories for the signed-in member's club. Flexible categories offer a resource calendar of occupied and blocked periods, plus editable start/end inputs converted from club-local time to UTC; session-required categories offer dated occurrences in a weekly planning. Conversation-enabled categories without sessions omit start/end inputs and submit an undated request; the member discusses availability in the reservation conversation. It shows loading skeletons, retryable category errors, validation/creation errors, and a receipt with the returned booking status and a reference. It never queries the staff reservation list. Creation and editing forms display non-blocking warnings when the start is in the past, more than 365 days away, or the duration exceeds 62 days. These warnings do not require confirmation and do not disable saving. The calendar query's 62-day fetch-window limit is independent of booking duration and remains unchanged. The form prefills the first available category, its name as an editable resource suggestion, tomorrow's local 10:00–12:00 slot, and one guest. These defaults do not claim availability. Resource suggestions follow category selection until manually edited. Background refreshes preserve the draft; a removed category requires a new selection. Changing club/member or starting another request resets the defaults. The same page lists the member's upcoming pending, booked, and confirmed reservations. Selecting one opens `myReservation(clubId, id)`, which returns only a reservation owned by the authenticated member. Pending, booked, and confirmed reservations that already have a time can be edited with `updateMyReservation(input)`; the same statuses can be cancelled with `cancelMyReservation(clubId, id)` until the reservation starts. From its `startsAt` on, `cancelMyReservation` returns `reservation: null` and `error: "This reservation has already started. Contact the club."` without any refund, and only staff can cancel it. A member update replaces the requested resource, window, and guest count, clears any stale staff proposal, and issues a new `paymentVersion`. Edits are rejected with an `error` when the reservation is completed or canceled (`"This reservation can no longer be edited."`), when a conversation-enabled category has accepted terms (`"Accepted reservation terms cannot be edited. Contact the club to change them."`), or when a confirmed reservation has reached its `refundDeadline` (`"The cancellation notice has passed. Contact the club to change this reservation."`). An unavailable window or resource returns `"This reservation cannot be changed to this time or place."`. ### Rescheduling a paid reservation [#rescheduling-a-paid-reservation] Editing the window or resource reprices the reservation with the same rules as creation: hourly, daily, and nightly resources charge per unit of the new window, while `once` prices stay unchanged. For `booked` reservations the new price simply replaces the unpaid amount. For `confirmed` and `completed` reservations, staff and member edits settle the difference on the member's credit balance in the same transaction: a higher price debits the difference (a `reservation` credit entry) and a lower price refunds it (a `refund` credit entry). An insufficient balance rejects the edit with `"Not enough credits. Top up your balance and try again."` and leaves the reservation unchanged. A later cancellation refunds the net amount actually paid. Edits that would change a collected security deposit, or the price of a legacy Stripe-paid reservation, are rejected. `myReservationChangeQuote(input: UpdateMyReservationInput!)` prices the same member edit without saving it, applying the same ownership, status, notice, availability and conflict checks as `updateMyReservation`. It returns `ReservationChangeQuoteResult { quote, error }` where `quote` has `previousAmount`, `amount`, `difference` (`amount - previousAmount`, in hundredths of a credit) and `settledInCredits`, which is true when saving would immediately debit or refund the difference. The member app shows this summary with the resulting balance and asks for confirmation before saving a booked or confirmed reservation whose price changes. ```graphql query QuoteChange($input: UpdateMyReservationInput!) { myReservationChangeQuote(input: $input) { quote { previousAmount amount difference settledInCredits } error } } ``` ### Cancellation notice [#cancellation-notice] `Reservation.refundDeadline` is the UTC instant before which a cancellation is fully refunded: the booking start minus the longest `cancellationNoticeDays` among its resources. It is null for undated bookings and resources without a notice. When `cancelMyReservation` runs at or after this deadline, the price (including credit-priced products) is not refunded and `Reservation.refundForfeited` becomes true; a collected security deposit is still returned. Only a paid booking (`confirmed`, or with a recorded card payment) can forfeit its price: a `booked` reservation canceled late before any payment keeps `refundForfeited` false. Legacy Stripe payments are not refunded either, and a later session cancellation does not refund them. `myReservation.creditPayment.refundableAmount` already excludes the forfeited price once the deadline has passed. A member cannot cancel a reservation that has already started, so a no-show can still be marked. Staff cancelling with `cancelReservation` or `updateReservation` at or after the deadline get the same outcome by default, and refund the price too with `refundLateCancellation: true`, for example when the club caused the cancellation. Before the deadline, staff cancellations refund in full whatever the argument. Session cancellations always refund in full. ## Pay a booked reservation [#pay-a-booked-reservation] Staff moves an accepted paid request from `pending` to `booked`. For conversation-enabled categories, only the member's acceptance of a paid proposal makes the reservation `booked`: staff cannot move those pending requests to `booked` or `confirmed` (`error: "Propose a time and wait for the member to accept it before booking."`), and a reservation without a time cannot leave `pending` except by cancellation. The authenticated reservation owner can pay with their club credits: ```graphql mutation PayReservation($clubId: ID!, $reservationId: ID!, $paymentVersion: ID!) { payReservationCredits( clubId: $clubId reservationId: $reservationId paymentVersion: $paymentVersion ) } ``` All three IDs must be UUIDs. Send the `paymentVersion` returned on the displayed reservation. The transaction rejects a changed version or an outstanding replacement proposal with `Reservation changed. Review it before paying.` and leaves the wallet unchanged. Refresh the reservation and require another explicit action; never retry with a newer version automatically. The server verifies ownership, club, active account and `booked` status. It debits both the snapshotted price and security deposit in one transaction and confirms the booking atomically. This also applies to a zero-price booking with a nonzero deposit. Insufficient credit leaves both the booking and wallet unchanged. Retries cannot debit a confirmed reservation twice. See [Club credits](/en/graphql/credits) for top-ups and balances. New cash Checkout requests are rejected. ## Staff booking and editing [#staff-booking-and-editing] `createStaffReservation(input: StaffReservationInput!)` creates a reservation directly for a club customer. Staff supplies the club, category, customer, resource, start/end, guests, and optionally status. Without status the reservation is `booked` when its computed price or deposit is positive and `confirmed` otherwise; the back office always omits it. A session booking (`sessionOccurrenceId`) may omit the resource: it always uses the session's resource. Omitted check-in/check-out values inherit the category offsets; explicit timestamps override them. The customer is either `personId`, a person of the same club, or `guestName` for a walk-in guest without a person record. Pass exactly one: both together, or a blank or longer than 100 characters `guestName` without `personId`, fail with `BAD_USER_INPUT`. A walk-in reservation stores the trimmed name as `guestName` with a null `personId`, uses the resource's base price, and sends no confirmation email. Walk-in guests can join sessions only when the session has no guest list; restricted sessions return `This session is reserved for its guest list.` A `personId` outside the club returns `This customer is not in this club.` When `personId` is a guest account, the member or staff user who invited that guest also receives an email with the resource and dates, unless they created the reservation themselves; a failed email never blocks the booking. `updateReservation(input: UpdateReservationInput!)` edits the booking window, resource, guests, status, and can set either check timestamp to `null` to remove it. Creation and updates accept elapsed booking windows and durations longer than 62 days. Timestamps must still be valid and increasing. The editor shows the same non-blocking date warnings as the consumer. For a booking linked to a session, `updateReservation` preserves its occurrence dates and the resource assigned to that session. Saving unchanged terms or canceling one participant remains allowed when other participants share the same occurrence. Raising `guests` must fit the session capacity alongside the other participants, as when staff add a participant; otherwise `error` is `This session does not have enough places left.` A smaller party is always accepted. To move the whole course, use `updateReservationSession`, which moves its participants and waitlist together. The workflow is `pending` → `booked` → `confirmed` → `completed`, with cancellation from any nonterminal stage. Staff cannot set `completed` with `updateReservation`; a reservation completes when its guest leaves (see [Departure and deposit refunds](#departure-and-deposit-refunds)). Reservations with both zero price and zero deposit may move directly from `pending` to `confirmed`. A paid reservation cannot be manually confirmed; its credit payment confirms it. Staff can edit the window, resource, and products of confirmed and completed reservations outside conversation-enabled categories; price differences are settled as described in [Rescheduling a paid reservation](#rescheduling-a-paid-reservation). Cancellation returns the original price and deposit debits once, in the same transaction as the status change. Uncollected deposits are never refunded. Existing cash payments retain their original Stripe refund flow. Completed and canceled reservations are terminal. A canceled reservation cannot be saved again, even unchanged: `updateReservation` returns `reservation: null` and `error: "A canceled reservation can no longer be edited."`, so a forfeited price or a refund is never replayed. ### Cancel a reservation [#cancel-a-reservation] `cancelReservation(clubId: ID!, id: ID!, refundLateCancellation: Boolean! = false): ReservationResult!` cancels a `pending`, `booked` or `confirmed` reservation and changes nothing else: an edit another staff member saved in the meantime is kept, unlike resending every field through `updateReservation`. It requires `reservations_manage`, and planning staff only reach reservations in their assigned categories. The refunds are those of a staff cancellation through `updateReservation`: a card payment is refunded through Stripe before the status changes, and a failed refund returns its message in `error` with the reservation unchanged. Credit debits for the price and deposit are returned in the same transaction. At or after the [cancellation notice](#cancellation-notice) deadline only the deposit is returned and `refundForfeited` becomes true, unless `refundLateCancellation` is true. `UpdateReservationInput.refundLateCancellation` makes the same choice when `updateReservation` cancels. The pending confirmation email is dropped, the outstanding proposal is cleared, and `paymentVersion` changes. An open Stripe Checkout is then expired; see [external reservations](/en/graphql/external-reservations). Canceling an already-canceled reservation succeeds and returns it unchanged, without refunding, returning credits or expiring a checkout again. Completed or unknown reservations return `reservation: null` with `This reservation cannot be canceled.` When `updateReservation` cancels a reservation whose full card refund already canceled it, it returns the canceled reservation without error. ```graphql mutation CancelReservation($clubId: ID!, $id: ID!, $refundLateCancellation: Boolean!) { cancelReservation(clubId: $clubId, id: $id, refundLateCancellation: $refundLateCancellation) { reservation { id status } error } } ``` `reservation(clubId, id)` loads one staff-visible booking. Its nullable `guestAvatar` is the linked member's photo, so staff without access to member profiles can still show it; other reservation queries return it as null. The back office links upcoming rows to that detail, where staff can edit the reservation. Conversation-enabled categories show a conversation tile on the same page with a fixed-height message history and composer. A pending request can be accepted by setting its status to `confirmed`, unless its category is conversation-enabled. For those, staff call `proposeReservationTime(clubId, id, startsAt, endsAt, free)`. Proposals are limited to pending or booked conversation-enabled reservations without fixed sessions or a cash checkout. Dates must be valid, increasing UTC timestamps with a future start. Before sending, the server locks the resources and rejects blocked or already occupied windows; proposed windows are not holds. The same availability checks run again at acceptance. Long future windows remain allowed. Every proposal exposes a nullable `proposalVersion` UUID on `Reservation`, alongside `proposedStartsAt`, `proposedEndsAt`, and `proposedFree`. Sending a proposal calculates `amount` from the proposed duration, member pricing and snapshotted products. `proposedAmount` holds this quote. For a booked reservation, `amount` and the accepted dates stay unchanged until acceptance. The existing deposit is retained. Identical resends preserve the version; changed dates, price or free choice replace it. Editing a pending request clears the outstanding proposal and its version. For conversation-enabled reservations, accepted dates, resources and product quantities cannot be changed through ordinary editing. Before payment, send a new time proposal for the same resource. After payment, a change to these terms requires a new request. Administrative edits and cancellation remain available. All dated staff edits check resource and category blocks as well as other bookings. Cancellation and completion do not require the period to remain available. Members call `acceptReservationProposal(clubId, id, proposalVersion)` with the version they displayed. The version is checked under the reservation lock. A stale version returns a null reservation and `Proposal changed. Review it before accepting.`; clients refresh and require another explicit acceptance. The mutation promotes the proposed dates and price, changes `paymentVersion`, and clears the proposal. Its price stays fixed even if catalogue prices change after the proposal was sent. `free: true` waives the price and product charges, but not the deposit. A positive price plus deposit produces `booked` (payment due); zero produces `confirmed`. Credit payment atomically debits the price and deposit before confirming. `ReservationResult.errorCode` is a nullable `ReservationErrorCode` enum. For `acceptReservationProposal`, `declineReservationProposal` and `withdrawReservationProposal`, `PROPOSAL_CHANGED` means the displayed version no longer matches; `PROPOSAL_UNAVAILABLE` means the proposal cannot be acted on (for example, expired acceptance or an already removed proposal). Both return `reservation: null` and the public `error` message. Unavailable does not disclose whether a reservation exists outside the caller's scope. Other business refusals and successful results have a null code. Validation and authorization errors still use GraphQL errors. Clients should read `errorCode`, not classify the English message; the web and consumer transports preserve it as `ApiResponseError.code`. These codes are result fields, not GraphQL `extensions.code` values. The member sees one proposal card with its dates, total, deposit when applicable, and acceptance action. Staff see the same total after sending. Past proposals cannot be sent from the editor or API. Errors leave the previous proposal unchanged. Creating a category never creates a reservation. ### Early check-out [#early-check-out] ```graphql mutation SetReservationCheckedOut($clubId: ID!, $reservationId: ID!, $checkedOut: Boolean!) { setReservationCheckedOut( clubId: $clubId reservationId: $reservationId checkedOut: $checkedOut ) { reservation { id checkedOutAt } error } } ``` Staff with `attendance_manage` on the reservation's category record that a guest has left before the scheduled end. `checkedOut: true` stores the current time in `checkedOutAt` and immediately counts as the reservation's end for the resource's [after-reservation checklist](#checklist), so housekeeping can start before the booked end. It is accepted only for a `confirmed` reservation between its check-in (or start) and its scheduled check-out (or end); otherwise `reservation` is null and `error` is `Only a confirmed reservation in progress can be checked out.` A reservation without a collected deposit becomes `completed` at check-out; one holding a deposit stays `confirmed` until the deposit is refunded. `checkedOut: false` clears `checkedOutAt` and returns the reservation to `confirmed`. It is accepted while the reservation is still `confirmed`, or `completed` by a check-out without deposit before its scheduled check-out (or end); otherwise it returns `This check-out can no longer be undone.` Repeating the current value returns the reservation unchanged without a history entry. The booked dates, price and attendance are not changed. Changes are recorded in [reservation history](#reservation-history) as `checked_out` and `check_out_undone`, alongside an `updated` entry when the status changes. On the reservation page, staff with this permission see **Check out** while `canCheckOut` is true and **Undo check-out** while `canUndoCheckOut` is true; the phase chip and a **Checked out** date card show the recorded time. ### Departure and deposit refunds [#departure-and-deposit-refunds] A reservation's departure is its `checkedOutAt`, otherwise its scheduled `checkOutAt`, otherwise `endsAt`. Once it has passed: * A `confirmed` reservation without a collected deposit becomes `completed` automatically. * A `confirmed` reservation holding a deposit stays `confirmed` until the deposit is refunded, by staff or automatically. ```graphql mutation RefundReservationDeposit( $clubId: ID! $reservationId: ID! $amount: Int! $message: String ) { refundReservationDeposit( clubId: $clubId reservationId: $reservationId amount: $amount message: $message ) { reservation { id status depositRefundedAmount depositRefundNote } error } } ``` Staff with `reservations_manage` on the reservation's category refund the deposit of a departed `confirmed` reservation. `amount` is in the deposit's unit (credit hundredths, or cents for card-paid [external reservations](/en/graphql/external-reservations)), from `0` to the collected deposit; the rest is kept by the club. `message` is optional, trimmed, at most 1,000 characters, and stored as `depositRefundNote` only when part of the deposit is kept. Negative amounts and longer messages receive `BAD_USER_INPUT`. The refund is credited to the member's wallet, or refunded to the card first for external reservations, then `depositRefunded` becomes `true`, `depositRefundedAmount` records the amount and the reservation becomes `completed`, in one transaction. A deposit that is not held, a reservation that has not departed or is already settled returns `This deposit cannot be refunded yet or has already been settled.`; an amount above the deposit returns `The refund cannot exceed the deposit.`, and card failures return the payment error. Concurrent requests settle the deposit once. The history records `deposit_refunded` with the `depositRefundedAmount` change and the message as its note. The customer is told by email, sent to the member's address or, once a temporary external account has been removed, to the email kept in `externalCustomer`. It gives the refunded and kept amounts and the message. Members also get an in-app **Security deposit refunded** notification linked to the reservation; the email respects their reservation email preference. Deposits still held are refunded in full automatically, without a message, once the club's delay after departure has passed. A background worker checks every minute. ```graphql query ReservationDepositRefundDays($clubId: ID!) { reservationDepositRefundDays(clubId: $clubId) } mutation SetReservationDepositRefundDays($clubId: ID!, $days: Int!) { setReservationDepositRefundDays(clubId: $clubId, days: $days) } ``` The delay defaults to 7 days and ranges from 0 (refund at departure) to 365; other values receive `BAD_USER_INPUT`. Both operations require `settings_manage`; changes are recorded in the activity log. Club administrators edit it under **Settings → Reservations → Security deposits**. On the reservation page, staff with `reservations_manage` see **Refund deposit** next to the deposit while `depositSettleable` is true. The dialog proposes the full amount; lowering it shows how much is kept and a **Message to the customer** field. ### Reservation history [#reservation-history] `reservationHistory(clubId: ID!, id: ID!, page: Int! = 1)` returns one reservation's timeline, newest first by time and ID, 20 events per page. It uses the same assigned-category staff access as `reservation` (`planning_read`). IDs must be UUIDs, `page` must be between 1 and 1,000,000, and a missing reservation or one from another club returns null. ```graphql query ReservationHistory($clubId: ID!, $id: ID!, $page: Int! = 1) { reservationHistory(clubId: $clubId, id: $id, page: $page) { page hasMore items { id occurredAt action actor actorName note changes { field before after } } } } ``` `action` is one of: | Action | Recorded when | `changes` fields | | -------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `created` | A member or staff creates the reservation | none | | `updated` | Status, resource, dates, check-in/out or price change | `status`, `resource`, `startsAt`, `endsAt`, `checkInAt`, `checkOutAt`, `amount`, `refundForfeited` | | `time_proposed` | Staff propose another time | `proposedStartsAt`, `proposedEndsAt`, `proposedFree`, `proposedAmount`, `amount` | | `proposal_declined` | The member declines the proposed time | proposal fields cleared | | `proposal_withdrawn` | Staff withdraw the proposed time | proposal fields cleared | | `attendance_marked` | Attendance changes | `attendance` | | `checked_out` | Staff record an early check-out | `checkedOutAt` | | `check_out_undone` | Staff undo an early check-out | `checkedOutAt` (cleared) | | `deposit_refunded` | The deposit is refunded, fully or partly | `depositRefundedAmount` | | `invitee_added` | Someone is invited to the reservation | `invitee` (email in `after`) | | `invitee_removed` | An invitation is removed | `invitee` (email in `before`) | `before` and `after` are raw stored values: status and attendance enum values, ISO timestamps, and `amount`/`proposedAmount` in the currency's minor unit. `refundForfeited` changes from `false` to `true` when a late cancellation keeps the price. Only changed fields are listed. Updates are recorded from every source, in the same transaction as the change: staff edits, member edits and cancellations, accepted proposals, credit and card payments, refunds, product consumption, session cancellations and session moves. Writes that change none of the listed fields add no event. `actor` is `staff`, `member` or `system` (payment webhooks and other automatic changes). `actorName` is the staff member's current name, or the reservation holder's name when they acted as a member; it is null for system events. `note` is the optional attendance note. Chat messages and per-staff conversation archiving are not part of the timeline. The back office shows this timeline in a **History** card on the reservation page and in the calendar drawer. It refreshes every 30 seconds, on tab focus, and after staff edits, attendance marks and invitation changes. ## Conversation API [#conversation-api] `reservationChat(clubId, reservationId)` and `postReservationMessage` are the professional operations used by the Champagne back office. The API enforces club access and, for staff, the reservation-category assignment. The reservation detail loads and posts messages without leaving the back office. For staff-role accounts, sending the first professional message also records that account as a participant. `reservationChats(clubId, archived)` powers the staff-only **Chats** page and returns only conversations in which the authenticated staff account has participated and whose category is still assigned to that account. Removing a category assignment also removes its conversations from both the active and archived inbox on subsequent requests. Each item includes the reservation category, booking member name, resource, start date, reservation status, archive time, per-staff unread state, and latest message. Owners and admins cannot use this personal inbox API. ```graphql query ReservationChats($clubId: ID!, $archived: Boolean!) { reservationChats(clubId: $clubId, archived: $archived) { id categoryId categoryName guestName resource startsAt status archivedAt unread lastMessage { senderName body createdAt } } } mutation ArchiveReservationChat($clubId: ID!, $reservationId: ID!) { setReservationChatArchived(clubId: $clubId, reservationId: $reservationId, archived: true) } mutation ReadReservationChat($clubId: ID!, $reservationId: ID!) { markReservationChatRead(clubId: $clubId, reservationId: $reservationId) } ``` Archiving is reversible and scoped to the staff account: it never deletes messages or changes another participant's inbox. Sending another professional reply restores the chat to that account's active list. A new message restores the chat for every other participating staff account and marks it unread until that account opens it (the sender's own professional message is immediately read). The demo coach account starts with two active conversations, including one unread conversation, and one archived conversation. Member-facing applications use the customer-scoped authenticated API surface: ```graphql query MyReservationChat($clubId: ID!, $reservationId: ID!) { myReservationChat(clubId: $clubId, reservationId: $reservationId) { id clubName categoryName reservation { id resource { id name } startsAt endsAt status } messages { id author senderName body createdAt } } } mutation PostMyReservationMessage($clubId: ID!, $reservationId: ID!, $body: String!) { postMyReservationMessage(clubId: $clubId, reservationId: $reservationId, body: $body) { id messages { id author senderName body createdAt } } } ``` Send the token returned by `loginMember` as `Authorization: Bearer <token>`. Both customer operations require that the session belongs to `clubId` and that the authenticated person owns the requested reservation. A valid member cannot read another customer's conversation. The query also returns `null` when the reservation category does not require a conversation. Bodies are trimmed and limited to 4,000 characters. The server assigns the author role and uses the stored guest name for customer messages, so callers cannot impersonate a professional. Messages are ordered by creation time and ID. Club subdomains do not expose a `/chat/<reservation-id>` route; consumer apps embed these operations in their own experience. ### Add a participant to a session [#add-a-participant-to-a-session] Pass the occurrence `id` from `reservationAvailability.sessions` as `sessionOccurrenceId` in `createStaffReservation`, together with the customer `personId` or walk-in `guestName` and the selected `resourceId`. Session-based categories require this field. The server uses the occurrence start and end times, validates that it is in the future, and checks capacity atomically. Staff participants in the same occurrence can share resources; conflicting reservations and blocked periods still prevent booking. Full, missing, past, or out-of-scope sessions return an error without creating a reservation. Refresh availability after saving to update remaining places. ## Floor plans [#floor-plans] Each reservation category can have an optional saved floor plan, shared by its club staff. Administrators check **Enable visualization** in the reservation category create/edit form only for suitable categories, such as pool loungers or restaurant tables. Types without an enabled plan display only the calendar. The back office displays an enabled plan directly below the calendar in **Reservations → category** (or **Planning → category** for assigned staff), without a separate display mode. Club administrators create and edit plans; staff assigned to the category can read them. Members and staff from other clubs cannot access these operations. The calendar cursor synchronizes the plan with the selected time range. `reservationFloorPlan(clubId: ID!, categoryId: ID!): FloorPlan` returns `null` before configuration. A plan contains `clubId`, `categoryId`, `revision`, `spaces` and `elements`. Each element has `id`, `kind`, `label`, optional `resourceId`, `objectId` and `points`, `x`, `y`, `width`, `height`, `rotation`, `spaceId` and `locked`. Coordinates use an expandable world with a 1000 × 700 initial viewport. Simple shapes (`rectangle`, `roundedRectangle`, `ellipse`, `triangle`, `lShape`, `hexagon`, `capsule`, `semicircle`, `uShape`, `polygon`) bind to a resource of the same club and category. A new resource is created on save only when `bookable: true` and the resource is omitted; `bookable: false` keeps the shape decorative. The active cosmetic library contains Wall, Column, Single door, Window, Stairs, Potted plant, Leafy tree, Hedge, Parasol, Pool and Counter. Legacy furniture and rectangular-planter assets have been removed. Generated library designs retain their artwork and can be placed as reservable objects. Line and rounded rectangle are no longer offered in the library; saved instances still render. Simple shapes support independent width and height, rotation and the plan palette; an ellipse with equal dimensions is a circle. Set `outlineOnly: true` on any closed shape for a transparent interior and colored outline. Lines have no fill. Each resource can appear only once per plan. Removing it from a plan does not remove the resource or any reservation. Deleting an unused resource through the category settings while visualization is disabled also removes its saved placements and advances the plan revision. Unrelated placements and resources are preserved. Linked library illustrations are retained as non-reservable objects. Resources with any reservation history cannot be deleted. ```graphql query PoolPlan($clubId: ID!, $categoryId: ID!) { reservationFloorPlan(clubId: $clubId, categoryId: $categoryId) { revision spaces { id name surface palette } elements { id spaceId locked kind label resourceId objectId x y width height rotation points { x y } } } } mutation SavePoolPlan($input: SaveFloorPlanInput!) { saveReservationFloorPlan(input: $input) { error plan { clubId categoryId revision spaces { id name surface palette } elements { id spaceId locked kind label resourceId objectId x y width height rotation chairs chairPositions points { x y arc radius } } } } } ``` `SaveFloorPlanInput` requires `clubId`, `categoryId`, `revision` and the complete `spaces` and `elements` arrays. Each space requires a unique UUID `id`, a distinct trimmed name (1–50 characters, case-insensitive), and a `surface`. Every element must reference an existing `spaceId` and supply a Boolean `locked` flag. There must be 1–12 spaces; resource bindings are unique across all spaces. Start with revision `0`; each successful save increments it. Stale revisions return an error without overwriting newer edits. An empty `elements` array clears the drawing while retaining at least one space. Saving is transactional and serialized with reservation-category edits. Validation allows at most 200 elements, unique UUID element IDs, labels of 1–100 trimmed characters, finite positions within ±1 billion, widths/heights up to 1 million (minimum 20/10 for reservables, 1 for non-bookable geometry), rotations of 0–359 degrees, and finite geometry. The world expands beyond the initial 1000×700 viewport; trimmed and rotated visible geometry still determines labels and editing handles. Positions, dimensions, rotations and shape-point coordinates use GraphQL `Float`, preserving fractional geometry and artwork aspect ratios. Resource and library-object references must belong to the selected category. When an `objectId` is supplied, its `resourceId` and internal `kind` must match the saved library object (`rectangle` for reservable, `zone` for decorative). An illustrated decorative object has no reservation resource; a reservable design may have a linked resource or defer resource creation to individual placements on save. Default cosmetic kinds are `partition`, `column`, `singleDoor`, `window`, `pottedPlant`, `leafyTree`, `hedge`, `parasol`, `swimmingPool`, `barCounter`, and `stairs`. Their artwork was generated through the same Sunburst backend generation and Auto-lines vectorization pipeline as user-created objects. They are real SVG assets shared by staff and member views, have no `objectId`, and create no reservation resource. They support proportional scaling and palette recoloring, but do not accept custom points or chairs. Existing legacy furniture artwork still renders; it is no longer offered as a cosmetic default. Native shapes use hand-coded SVG geometry, not image generation. Capsule, semicircle, U-shape and freeform polygon presets are editable with the shape editor; Rounded rectangle, Capsule, Line / polyline, and Rectangular planter are no longer offered in the library; their saved kinds remain supported. All native shapes use constant-width outlines. Freeform polygon placement opens its shape editor immediately, and double-clicking an unlocked native shape in edit mode opens the same editor. Clicking its body keeps editing active. Reset shape requires confirmation, restores the preset outline, preserves position/dimensions/rotation, and is undoable. Points can move outside the current object frame: the editor expands and repositions the frame, then normalizes the resulting anchors back into 0–100 coordinates. The shape can expand beyond the initial viewport within numeric safety limits. Crossing or overlapping non-adjacent edges are rejected; curved outlines are checked using sampled arc geometry. Generated PNGs are cropped to their visible alpha bounds with a four-pixel transparent safety margin before vectorization. Default generated SVG viewBoxes and their PNG companions are cropped too. Existing built-in artwork placements are fitted to their actual aspect ratio while keeping their visible center and rotation unchanged. View and edit modes use the same canvas width at each viewport size. Basic shapes (`rectangle`, `roundedRectangle`, `ellipse`, `triangle`, `line`, `lShape`, `hexagon`, `capsule`, `semicircle`, `uShape`, `polygon`) accept `points`: 3–64 anchors, or 2–64 for open lines, with floating-point `x` and `y` coordinates from 0 to 100 relative to the object's dimensions. Closed shapes must enclose an area. Omitted points use the kind's preset. Rectangle, triangle, L-shape and hexagon presets have softly rounded corners (8% of the shorter dimension); circles retain their natural curvature and lines have round caps. Explicit custom points and corner radii are preserved. The staff and member views share the same geometry and color definitions. Other kinds and library artwork reject custom geometry. Each anchor optionally stores `radius` (0–100, a floating-point percentage of the shorter object dimension) or `arc: true` for a circular bulge through the neighboring endpoints. Adjacent bulges and simultaneous radius/bulge settings are rejected. Radius geometry is clamped to adjacent edge lengths. Points retain their relative positions when dimensions change. Shape point coordinates, corner radii and element rotation use GraphQL `Float` in both inputs and responses, as do element positions and dimensions. Fractional geometry is preserved through save and reload without rounding. Existing ranges still apply, including rotation from 0 to 359 degrees. `chairs` and plan `revision` remain integers. Reservable shapes (`rectangle`, `roundedRectangle`, `ellipse`, `triangle`, `lShape`, `hexagon`, `capsule`, `semicircle`, `uShape`, `polygon`), including reservable library placements, accept `chairs`, an integer from 0 to 16. Null, omitted or zero means no attached chairs on every kind, including imported lines and decorative furniture. Furniture and other kinds reject positive chair counts. An explicit `chairPositions` array must still match the count; only an empty array is valid for zero chairs. Chairs follow the shape outline and remain fixed-size when dimensions change; they are visual seats, not independent reservation resources. Library placements use their rectangular frame rather than artwork alpha. The former pool kind and ladder setting remain removed; the new `swimmingPool` kind is cosmetic artwork. `FloorPlanElement` and `FloorPlanElementInput` accept nullable `chairPositions: [Float!]`. Omitted or null uses the automatic layout; existing automatic positions are preserved when collision-safe. An explicit array contains exactly `chairs` finite numbers in `[0, 1)`, in chair identity order, measuring normalized perimeter arc length. For example, a sharp-cornered 100-by-100 square with four chairs can use `chairs: 4, chairPositions: [0.125, 0.375, 0.625, 0.875]`. An empty array is valid only when the chair count is zero. Ellipses start at the top and proceed clockwise; custom outlines start at their first segment after the first rounded corner and follow point order. Fractions are local to the object and independent of translation and rotation. They persist unchanged in the plan's JSON elements and are returned by staff/member plan reads and save responses when selected. Saving rejects mismatched counts, out-of-range fractions, overlapping chairs and chairs intersecting the object's outline, including concave and curved portions. Automatic layouts are also checked: unsafe defaults are relocated, and insufficient space returns a validation error without saving a partial plan. Explicit layouts are never silently repaired. Changing dimensions, points or chair count requires a valid updated array, or null to restore automatic placement. Collision checks conservatively enclose chair artwork and strokes; they concern the parent object and its attached chairs, not unrelated plan objects. Very complex/large curves can exceed the bounded geometry budget and are rejected rather than placed unsafely. Plan spaces retain their palette (`lagoon`, `terracotta`, `grove`, `dusk`). Simple shapes, built-in illustrations and generated objects all use the same surface, secondary, accent and outline colors. Generated previews, library thumbnails, canvas objects and the member plan follow the active space theme. Vector fills are mapped directly to those colors; raster fallbacks use an alpha-preserving palette lookup filter rather than approximate hue rotation. Original artwork bytes are unchanged, so the same object can follow different themes in different spaces without regeneration. ```graphql query PoolOccupancy($clubId: ID!, $categoryId: ID!, $from: String!, $to: String!) { reservationFloorPlanOccupancy(clubId: $clubId, categoryId: $categoryId, from: $from, to: $to) { resourceId guestNames bookings { guestName startsAt endsAt } } reservationAvailability(clubId: $clubId, categoryId: $categoryId, from: $from, to: $to) { sessionCount sessions { id startsAt endsAt remaining } blocks { id resourceId startsAt endsAt reason } } } ``` Occupancy accepts an increasing timestamp range of at most 31 days. It returns occupied resource IDs, guest names and individual bookings with their scheduled `startsAt` and `endsAt` timestamps (without check-in/check-out margins), with no pagination or client-side sorting arguments. Every non-canceled reservation overlapping the range counts, including its check-in/check-out margins. Intervals are half-open: a reservation ending at the range start does not occupy that range. The plan combines occupancy with blocked periods that are category-wide or scoped to the resource and, for session-based categories, a session covering the whole range with remaining capacity. Pending or failed data appears as unknown, never free. The screen refreshes availability automatically every 30 seconds. It displays times in the browser's local time zone. The studio supports up to twelve named plans (stored in `spaces`) per category, drag positioning, multi-selection, alignment, duplication with unused resource bindings, locking and undo/redo. The library has **Reservables** and **Customizable** tabs, with Reservables selected by default. Switching tabs cancels any pending placement. A fixed-height header reserves space for the Customizable generation button, keeping library spacing stable when it appears. Reservables contains Rectangle, Circle, Triangle, L-shape and Hexagon; Customizable contains generated furniture, palm trees and parasols. Built-in artwork uses simple flat orthographic plan views. User-generated artwork follows the Gemini Pro top-down prompt and transparency-only matting described below. The object inspector exposes width and height sliders for all objects and a chair count for reservables. Both sliders use the same stable display scale (0–1000 for ordinary objects, expanded for larger imported objects), so equal dimensions align and moving an object does not shift their thumbs. Actual dimensions can expand up to the numeric safety limit without clamping at the viewport edge or changing position. Changes preview live and each adjustment is one undo operation. Shapes offer **Keep aspect ratio**, saved as the optional `keepAspectRatio` boolean on each element (enabled by default; null or omitted is treated as enabled in the editor, while explicit false unlocks dimensions). When enabled, either dimension slider or the resize handle scales both dimensions together, bounded by the numeric dimension safety limit while preserving fractional dimensions and exact aspect ratios. Furniture images preserve their proportions and have baked-in chairs. **Edit shape**, at the bottom of the inspector after dimensions, chairs and resource assignment, toggles editing directly on the plan. Selecting another object exits this mode. Drag anchors to reshape with horizontal/vertical snapping, drag the inset circular handles to round corners, and double-click an anchor to toggle a circular bulge. Plus controls add points on straight edges; Delete removes a focused anchor, respecting the minimum point count. Arrow keys move focused anchors or adjust a focused radius handle. Plan undo/redo includes these changes. Reset shape restores the selected kind's preset. The lock action displays an unlock icon when the selection contains locked objects. The floor-plan view reuses the resource calendar (day/week). Its optional time cursor selects an instant in 15-minute increments. The UI queries a one-millisecond half-open interval at that instant, without a duration control. A single vertical guide aligns with the existing reservation rows. Calendar navigation preserves the selected hour, and opening the plan carries the calendar date and view. Cursor dragging is settled for 150 ms before occupancy queries; old availability is marked unknown while the selected time is changing. Layout editing remains independent of time navigation. No GraphQL operation or authorization changes are required for this calendar control. The Reservations section keeps its resource-category tabs and settings. The calendar spans the full width, with the floor plan immediately below it only when enabled for this category. The calendar remains visible while editing the plan. The URL retains the calendar date and day/week view. Creating and opening reservations remain available from the embedded calendar. The `ReservationCategoryInput.visualizationEnabled` boolean defaults to `false` and is returned on `ReservationCategory`. It is saved with the other category settings by the existing create/update mutations. Disabling visualization hides both the plan and time cursor without deleting the saved layout. Re-enabling restores that layout. In the editor, drag a furniture tile onto the canvas. A ghost preview follows the pointer in canvas coordinates. Dropping outside the visible SVG cancels; valid drops are centered in world coordinates without a fixed placement boundary. Selecting a tile then tapping a location is also supported, as are Enter on the canvas to place centrally and Escape to cancel. Every placement is undoable and a resource cannot be placed twice. New built-in shapes and reservable library objects start with the object's name and no resource binding. Placing a reservable immediately focuses a resource-name field beneath the shape. Enter or blur commits a trimmed name; Escape cancels the edit, and blank names retain the previous value. The name is used for resource creation on save. When an unplaced resource is available, the resource dropdown offers it but defaults to creating a new resource; existing resources precede that option. Every reservable placement shows the resource dropdown, including its current resource, unused resources, and the create-on-save option. The inspector assigns resources instead of renaming them. Selecting an existing resource changes the placement label to its resource name. The category modal exposes the visualization checkbox but does not edit resources. The editor uses the built-in object library. Existing stored custom objects remain visible and manageable, but the editor no longer creates or generates new ones. The separate always-visible ListBox displays each placed object's chosen label, library name and thumbnail. Existing category update operations still manage ordinary resources. Reservable floor-plan elements may omit `resourceId`. Saving creates their reservation resources atomically with the plan and returns the assigned IDs. Resource names use the element label, with a ` #2`, ` #3`, etc. suffix when that name is already used in the same reservation category. Other categories may reuse the name. Subsequent saves must retain the returned IDs; supplied IDs must belong to the same reservation category. Revision conflicts create no resources. The editor creates places by dropping reservable shapes onto the plan. Existing unplaced ordinary resources are reused first; newly created places appear in the calendar after saving. A resource with no assignment shows a create-on-save note; assigned resources show no note. ### Custom objects [#custom-objects] The following staff-scoped read returns previously stored custom objects for the club and reservation category; it has no filter, sort or pagination arguments. The **+** button appears only under Customizable and opens the object generator. Club administrators can rename or delete existing custom objects and rename ordinary reservation resources. If an artwork object is missing or temporarily unavailable, its `image` is `null`; the plan and its other objects remain available with a standard-kind illustration. Built-in, existing, and newly generated artwork all follow the active space palette at render time, including artwork marked `data-plan-render="raw"`. That marker preserves editing provenance, not a display-color override. Simple shapes and floor areas use the palette's `secondary` interior tone, with `outline` reserved for their strokes and `surface` for light trim. `generateFloorPlanObjectArtwork(clubId: ID!, categoryId: ID!, name: String!, traceId: ID, autoLines: Boolean! = true, generationModel: FloorPlanArtworkGenerationModel! = SUNBURST)` requires `catalog_manage` permission, valid UUIDs and a trimmed subject name of 1–100 characters without null bytes. It checks the category, duplicate names and 200-object limit before calling OpenRouter. There is no initial reference-image argument or drawing step. The modal shows **Object name**, **Auto-lines**, **Generation model**, **Brush model** and **Generate object**, with the standard cancel action. Auto-lines defaults on: source-guided centerlines produce non-scaling outlines over themed colored fills. With `autoLines: false`, Potrace traces colored fills and opaque navy ink ribbons instead; this is a different vectorization algorithm, not hidden outlines or embedded raster. The choice applies to new generation and legacy embedded-raster vectorization during refinement. Existing vector previews are not retraced or changed when the switch changes. The modal says **Applies to next generation** beside an existing preview. Changing either control never calls a paid provider. Settings remain local to the modal across attempts, and conflicting controls are disabled while pending. Failed attempts retain the preview and undo/redo history. Initial generation defaults to **GPT Image 2.5 Sunburst** (`openai/gpt-image-2.5-sunburst`) or uses **GPT Image 2.5 Flare** (`openai/gpt-image-2.5-flare`) with `generationModel: FLARE`. The `FloorPlanArtworkGenerationModel` enum allows only `SUNBURST` and `FLARE`; null and arbitrary model IDs are rejected before provider calls. Both IDs and image output support are verified against `/api/v1/models?output_modalities=image`. The generation selector persists within the modal, is disabled while pending, and applies only on the next **Generate object** request. It never triggers generation on selection or changes the preview, brush model, or DeepSeek classifier. Both use the same prompt and medium-quality, 1K, square PNG request through OpenRouter, with transparency-only backdrop removal. Before returning the initial preview, the server traces the PNG with native Potrace into real multicolor vector paths. The original PNG remains a private companion; it is never embedded in the returned SVG. Only magic-brush refinement uses **Gemini 3.8 Flash** (`google/gemini-3.8-flash`) by default, or **Gemini 3.5 Flash Lite** (`google/gemini-3.5-flash-lite`) selected with `editorModel: GEMINI_FLASH_LITE`, or **GPT-6 Luna** (`openai/gpt-6-luna`) with `editorModel: GPT_LUNA`, or **GLM 5.3 Flash** (`z-ai/glm-5.3-flash`) with `editorModel: GLM_FLASH`, or **GPT-6.1 Sol** (`openai/gpt-6.1-sol`) with `editorModel: GPT_SOL`. Sol uses the existing Low/Medium/High thinking choices and throughput-sorted compatible-provider routing. The server enum `FloorPlanArtworkEditorModel` allows only `GEMINI_FLASH`, `GEMINI_FLASH_LITE`, `GPT_LUNA`, `GLM_FLASH`, and `GPT_SOL`; arbitrary model IDs and null choices are rejected by GraphQL before provider calls. All support image inputs, required tools and low reasoning (verified against OpenRouter's live catalog). Gemini 3.8 Flash is pinned to **Google Vertex Priority** (`google-vertex/global/priority`) with required parameter support and provider fallbacks disabled on every edit and review turn. This does not change the chosen thinking effort or routing for the other models. GLM uses throughput-sorted routing for its first call with provider fallbacks disabled. The provider reported by OpenRouter is then pinned via `provider.only` for all subsequent turns of that attempt, including token-budget retries and visual review. Missing or changed provider identity fails safely. This uses OpenRouter's current fastest-compatible routing rather than an unverified fixed provider ranking; a new attempt selects anew. GLM supports `LOW`, `HIGH`, and `MAX` thinking effort, not `MEDIUM`. Other editor models retain `LOW`, `MEDIUM`, and `HIGH`. Unsupported model/effort combinations fail before model dispatch, and switching models resets an incompatible selection to Low in the UI. Generation, tracing, brush-model, thinking-effort, and classification controls are inside the create-object modal's collapsed **Expert mode** section between the name field and generation button. If it is closed when Generate is clicked, the entire section is hidden during generation and with its result; if open, it stays available. Changing the name or a failed initial generation restores access to it. The action becomes **Regenerate object** when a generated preview exists. The **Show more information** switch has its label on the right, and both its track and label are clickable. Its value is captured when Generate is clicked: if on, Debug appears beside Cancel, and detailed stages/timing are available in the visible expert section. Toggling the switch alone does not reveal Debug, even after an older generation. These details are hidden by default. The preview skeleton appears immediately at request start rather than waiting for the mutation state update. Loading and the first browser render use a preview skeleton with a 45-degree shimmer, not a progress bar. Clear sketch and Apply changes sit inside the preview, as does the brush tool, with a native **Reshape by drawing on the object** tooltip; the visible box edges are the drawing limits. A four-percent margin around the artwork is applied consistently to the UI and backend reference/review PNG framing. The information icon in the modal's top-right corner is reserved for a future help modal and is currently disabled. The brush model choice does not change the generation model or interpretation setting. Editing runs through OpenRouter's Chat Completions API, with providers sorted by highest throughput and required parameter support, after a **DeepSeek V4.1 Flash** (`deepseek/deepseek-v4.1-flash`) vision intent decision by default. The **Classify brush intent** switch is on by default (**DeepSeek + Gemini**). Turning it off (**Gemini only**, `classifyBrush: false`) skips DeepSeek entirely, including its geometric evidence computation. For Luna the labels are **DeepSeek + GPT-6 Luna** and **GPT-6 Luna only**. The selected editor model infers and edits in the same initial `update_svg` request; there is no extra editor classification call. Its comprehensive SVG prompt covers cut, fill (extension), erase, reshape and cut\_fill, prioritizing scribbles, then broad surrounding trails, then local inside/outside edges. It preserves unmarked geometry and opaque surfaces, uses real transparency for exterior space and gaps, and receives the original PNG, red brush guide and current SVG, not just scalar measurements. The request-local Boolean is non-null and defaults to true for omitted arguments; null or non-Boolean values are rejected before dispatch. The modal retains the setting across attempts and disables it with other conflicting controls while pending. Changing it never generates, refines or replaces a preview. All five editor models retain the identical bounded render/review loop below, in either interpretation mode. The SVG agent uses `update_svg(operations: SvgOperation[])`, which applies up to 64 atomic, ID-targeted operations to the current SVG, then renders it to a transparent 1024-by-1024 PNG for validation. Brush editing requires **bounded visual review**: Gemini receives the actual rendered PNG in a subsequent request, separately labelled alongside the unchanged original source image, brush guide and current SVG. It must explicitly `finish({})` after inspection, or correct concrete visible damage (collapsed geometry, unintended disappearance, broken fills/holes or lost unmarked features), not regenerate for aesthetic polish. The server permits one initial valid batch and at most **two valid corrective batches**, with a required final inspection after every correction. There are at most **eight model calls**, including validation repairs, output-length retries and acceptance. Transient HTTP retries remain bounded separately. Invalid batches leave both the committed SVG and PNG unchanged; a rejected correction must be repaired before acceptance. Premature finish is rejected. No new batch is committed on the last call or with less than 15 seconds left in the 600-second agent budget. Generation and refinement workflows and browser requests each have a ten-minute (600-second) deadline. Budget exhaustion, cancellation or missing acceptance produces a typed failure, never an automatically accepted last render. The previous editor preview stays intact. Vision review reduces obvious damage but does not guarantee detection of every defect. The editor shows the visually accepted result as one undoable preview revision; it does not start a second generation/review request. While waiting, it retains the previous preview and disables duplicate actions. Repeated delivery of the same preview token does not add duplicate undo entries. Provider calls and SVG validation remain on the server; no model credentials enter the browser. The **Thinking effort** selector sets `thinkingEffort: LOW | MEDIUM | HIGH | MAX` (`FloorPlanArtworkThinkingEffort`, default `LOW`, choices depend on the model). It applies to all five editor models for every edit, repair, and visual-review turn. Changing it starts no request and does not affect Sunburst/Flare generation or DeepSeek classification. Controls lock while pending. Higher effort can increase latency and cost; deadlines are unchanged. Brush editing uses a 32,768-token combined output budget and the selected reasoning effort (default `low`), matching the benchmark configuration rather than requesting a large reasoning budget that can exhaust the interactive deadline. A `finish_reason: length` response can retry once with 65,536 output tokens without applying partial output. Exhaustion fails rather than accepting unfinished artwork. Every element receives a deterministic unique ID; existing IDs are preserved across edits. Duplicate source IDs fail before model use. The model no longer copies old path strings to locate changes. Supported operations are: * `set`: `{ op: "set", id, attributes }` updates supported attribute strings. * `unset`: `{ op: "unset", id, names }` removes supported attributes to restore inheritance. * `remove`: `{ op: "remove", id }` deletes an element and its descendants. * `append`: `{ op: "append", id, parentId, tag, attributes?, beforeId? }` adds a new uniquely identified primitive/group, optionally before a direct child of the parent. For example, a batch can resize a known rectangle and delete its superseded outline: ```json { "operations": [ { "op": "set", "id": "seat", "attributes": { "width": "700" } }, { "op": "remove", "id": "old-outline" } ] } ``` IDs must come from the actual current SVG. Append IDs must be new and cannot be reused within the same batch, including after a removal. IDs, namespace, viewBox and engine metadata are immutable; the root and its display dimensions cannot be removed or changed. Initial generation appends geometry to the empty root ID. There are no raw XML insertions or search/replace operations. Each batch is limited to 100 KB. An invalid operation, invalid SVG or failed rendering preserves the whole previous SVG/PNG. The server enforces the 64-operation and attribute-name count limits; tool-schema descriptions state those maxima rather than using nested `maxItems`, which Gemini rejects for this schema. Validation feedback includes a zero-based operation index, error code, affected ID, match count when applicable, and a bounded summary of the relevant committed element. A render-level error uses a null operation index instead of blaming an arbitrary operation. Every request is a fresh compact context with the current SVG exactly once, the original task/reference and only the latest failed batch/error. Long failure payloads are explicitly truncated, with a focused failed-operation excerpt. Previous rejected batches and signed reasoning history are not replayed; review mode keeps only the current rendered preview. This avoids accumulating repeated copies of the SVG during repairs. Edits are instructed to match the object's existing corner radii, rounded joins/caps and inner-border rounding. The renderer accepts geometric primitives, groups, and root-local geometric clip definitions, at most 100 KB and 1000 elements, preserving the source viewBox and rendering into a 1024-square canvas. Scripts, external resources, embedded images, CSS, entities and document declarations are rejected before rendering. The tool schema, system contract and renderer share one attribute allowlist. Geometry/paint attributes, transforms, stroke caps/joins, fill rules, vector-effect, and fill/outline roles are supported. Safe `display` values are `inline` and `none`; `opacity`, `fill-opacity` and `stroke-opacity` accept decimal strings from 0 to 1. Use `remove` for deletion rather than hiding geometry when possible. Attribute values are strings, numeric geometry must be finite, and dimensions/radii/stroke widths cannot be negative. Only `clip-path="url(#local-id)"` references to validated source-space path definitions are allowed. Style attributes, classes, external URL references and event handlers remain forbidden even though these safe presentation attributes work. Generation asks for one simple, flat, strict 90-degree top-down object with large pale-blue regions, no surroundings, props, perspective, shadows or texture. Rectilinear parts have crisp edges and only tiny fillets; naturally curved silhouettes stay curved. Configure server-side `OPENROUTER_API_KEY` and floor-plan storage. No OpenAI key is required, including for scribble removal. Provider credentials never reach the browser. Initial generation and brush refinement each have a ten-minute (600-second) workflow deadline, including preprocessing, optional classification, model requests and vector conversion. Provider requests and the SVG loop cannot outlive that shared workflow budget; there is no shorter model or classification deadline. Native tracing/rendering retains its own safety limits. Browser calls allow 600 seconds including network and response overhead, so a browser timeout can occur slightly before the server workflow deadline. Preview storage and authorization are outside the model workflow budget but inside the browser deadline. GraphQL disables Bun's per-request socket idle timeout (which cannot exceed 255 seconds), without replacing the incoming request signal. Disconnecting or cancelling the browser request still interrupts the Effect and aborts provider fetches. Workflow timeout failures return `The artwork request timed out. Please try again.`; the browser deadline shows a translated timeout danger toast with Retry. Intentional cancellation does not produce an error toast, keeps the previous preview/sketch, and ignores late results. Cancel stays available for generation and brush refinement; creating/saving an object remains non-cancellable. Artwork mutations do not automatically retry on the client; errors use the shared toast with an explicit Retry action. Spending-limit failures return `Image generation spending limit reached.` and do not change billing settings or create preview files. After generation, **Magic brush** enables painting over the generated artwork. The user paints an approximate new edge and chooses **Apply changes**; there is no written refinement prompt. Space starts/finishes keyboard strokes, arrow keys draw and Escape leaves the tool. Undo/redo restores older private previews without calling the image provider again. Palette changes also require no generation. `refineFloorPlanObjectArtwork(clubId: ID!, categoryId: ID!, name: String!, artworkRef: ID!, strokes: [FloorPlanArtworkStrokeInput!]!, traceId: ID, autoLines: Boolean! = true, editorModel: FloorPlanArtworkEditorModel! = GEMINI_FLASH, classifyBrush: Boolean! = true, thinkingEffort: FloorPlanArtworkThinkingEffort! = LOW)` uses the same permission and exact club/category/name scope as generation. It reads the existing private preview; clients cannot supply replacement image data or remote URLs. Missing, consumed, foreign or malformed preview tokens are rejected before any image request. A stroke has `width: Float! = 0.08` and `points` containing `x: Float!` and `y: Float!`. Points are normalized to the padded square artwork workspace, in `[0, 1]`; widths are in `[0.015, 0.3]`. There must be 1–16 strokes with 2–512 finite points each. The workspace is the entire preview box, not the inset artwork viewport. Artwork keeps its centered 68% viewport with 4% internal padding (62.56% longest-side coverage of the box). Server reference rendering uses the identical expanded square frame; margin strokes remain outside the artwork rather than being clamped to its edges. Only gestures beyond the box are clamped to `[0, 1]`. The editor submits width `0.0544` to retain its previous on-screen brush footprint. The stored SVG viewBox and geometry are unchanged by this display transform. The brush trail is red (`#ff0000`) in the editor and unmasked model references. With classification enabled, the server computes alpha-relative trail measurements and sharp direction reversals from the unannotated artwork. DeepSeek's chat-completions API receives the current reference PNG and the red annotated PNG, with these measurements as supplementary evidence. The exact model's live OpenRouter catalog explicitly lists image input (verified October 8, 2026). A strict JSON schema and server validation select one of five intents: inward cut, outward fill, scribble erasure, broad outline reshape, or combined cut and fill. Invalid classifier responses stop the edit; no silent fallback or paid retry occurs. Its confidence is not a calibrated quality guarantee. Each intent selects a separate operation-specific prompt. Erasure explicitly distinguishes restoring an object's surface from removing detached exterior noise without changing the object's outline. All five intents send the actual stored SVG text as the committed initial patch state, plus one annotated PNG rendered from that same SVG with the red trail. The model edits existing vector geometry rather than reconstructing it from pixels. Name-only generation uses Sunburst or Flare raster artwork, never an LLM-drawn input vector. Existing true vector companions retain their geometry and fills. A legacy SVG containing one full-canvas embedded PNG is converted automatically, without a model call: the validated image and vector overlays are rasterized in their existing coordinate frame, then partitioned into connected flat-color regions. Coherent source-color areas are averaged before their fill pixels are quantized to the four canonical theme tones and relabeled for tracing. This avoids turning texture noise into palette stippling or separate vector regions for shades that display identically. Equal-color regions merge, and small enclosed color islands are reassigned to their surrounding tone without deleting detached silhouettes or transparent cutouts. Bounded categorical smoothing protects painted veins and exterior boundaries. Auto-lines uses a shared fitted boundary graph for fills: neighboring colors reuse the same curve with reversed controls instead of independent approximations. When a shared graph would exhaust the SVG byte budget or cleaned fills fail fidelity, the uncleaned, merged theme partition can be traced by Potrace instead; the inspector records `vectorization-fill-fallback` with the size or fidelity reason. The same SVG size and fidelity limits still apply, with no raster fallback. Auto-lines off retains Potrace 1.16 fill-mask tracing and filled painted-ink ribbons. Compound paths use even-odd fills for holes. New generation defaults to Auto-lines. Enclosed patches below the tracing alpha cutoff become opaque light-blue theme surfaces when their connected component contains no fully transparent pixels and does not reach the exterior. Fully transparent cutouts and exterior antialiasing remain untouched. Fidelity checks compare against this explicitly repaired input. The `source-centerlines-v1` TypeScript pipeline extracts a pixel skeleton and recenters it between opposing source ink edges. It preserves long straight runs, fits curves and circular contours, and keeps branch endpoints shared. Source alpha boundaries close incomplete exterior paint without filling genuine transparent cutouts. Ink detection adapts to the darkest supported colors, so soil remains a surface. Painted ink becomes real SVG paths with no fill, a 2-pixel stroke, round caps/joins, and `vector-effect="non-scaling-stroke"`. Fills are partitioned against these actual fitted lines. Their closed graph faces supply the exact same curves to a source-space clip path; fills cannot spill outside the visible border. Strokes are not clipped. The staff and member renderers namespace clip IDs per artwork instance and render strokes inline at a fixed thickness. Local clipping references are validated against plain path definitions; external references, recursive clips and executable SVG stay forbidden. Debug vectorization events identify `source-centerlines-v1` and preserve before/after evidence. Saved vector previews are not rewritten. Refitting is bounded by the existing fidelity check; ambiguous source marks can still produce spurs or imperfect junctions. Two bounded curve tolerances are available for silhouette validation. Tracing frames are limited to two million pixels; larger/over-complex legacy wrappers fail safely rather than silently changing their coordinate frame. The result must fit the SVG size/element limits and pass the existing bidirectional silhouette/detail check. Failure stops before paid classification; the edit loop never receives an embedded image or a silent raster fallback. No native mask or OpenAI image call is made. The erasure prompt distinguishes deleting a separate component, deleting an attached protrusion, restoring an underlying surface and clearing detached noise. The original and gestures register to the same centered square workspace. No undo history or separately competing source images are sent. The common rendering instructions explicitly treat the trail as a **quick freehand suggestion, not an exact path or mask**. The model infers the intended boundary or feature change, straightens shaky runs and eases curves. The name scopes the private preview but is not included in the edit prompt, so a name describing the original shape does not override the trail. The editable SVG is rasterized locally with genuine transparency. There is no model-generated chroma-key backdrop or palette recoloring. Deterministic tracing is used after initial image generation and for legacy raster-backed input. Provider and protocol errors fail safely. Each refinement normally makes one DeepSeek vision request (zero with `classifyBrush: false`), one Gemini edit request and at least one subsequent Gemini rendered inspection request. Additional model turns are for bounded visual corrections or validation repairs, up to eight Gemini calls total. Progress in direct mode starts with rendering, then `inferring-edit` ("Interpreting and editing brush strokes"), never a fictional `classifying` step; subsequent inspection/correction/repair stages are unchanged. Successful and failed traces record the interpretation choice and editor model, without credentials or private reasoning. It leaves the preceding SVG preview available for undo. Model edits can still change unmarked details; it is not a pixel-exact preservation guarantee. Gemini HTTP 408, 429 and 5xx responses retry up to twice per request with exponential backoff/jitter or `Retry-After`. A requested wait above 30 seconds fails rather than retrying prematurely; authentication, billing, validation and other permanent HTTP errors do not retry. Provider error completions returned inside HTTP 200 are normalized to their embedded HTTP error code, or 502 when none is supplied, and use the same bounded retry policy. Backoff and retries honor cancellation and the shared deadline. HTTP and token-budget retries can add paid requests; usage for failed provider calls is not always returned. The browser receives validated, actual vector geometry marked `vector-v3` and `data-plan-render="raw"`, not an embedded-PNG wrapper. Both staff and member viewers map its fills and existing outlines into the active palette; grouped/raster display fallbacks use the same alpha-preserving palette filter. They do not invent extra outlines. The SVG retains its source viewBox and geometry, with square display dimensions matching the brush workspace. A rendered PNG remains a private companion, not the editable source. SVG IDs may contain safe underscores, hyphens, periods and colons for stable patch targets. The editable SVG and rendered PNG companion are retained while pending and after confirmation, and deleted together with the library object. Existing non-raw SVG objects retain their established local normalization path. Bounds are 12 MB and 16 million decoded pixels at PNG decoding and two million at tracing; generated SVGs are limited to 100 KB and 1,000 elements. Tracing averages colors within regions and approximates raster edges: texture, gradients, subpixel details and very narrow gaps are not pixel-exact. Silhouette validation is not a semantic or full color-fidelity guarantee. Failures never return a fake raster-backed SVG. Generation and refinement return `artwork` (a safe SVG data URL), `artworkRef` (an opaque UUID), `error`, and nullable `durationMs` (server processing time). The private preview does not expire and is bound to the exact club, category and trimmed name. The modal contains a name field and generation action, then a generated preview with the optional **Magic brush** and undo/redo controls. There is no category field or **Can be reserved** switch. Objects created through this modal are decorative and appear in Customizable. Changing the name invalidates the preview; creation requires confirmation. The plan object list shows only the placed resource name, without the duplicate object name/type subtitle. Magic-brush edge trails reshape the indicated boundary. Dense back-and-forth, crossing or looping scribbles remove the targeted feature (for example, stairs) and restore the surrounding surface, without cutting a hole or changing unmarked features. Previous preview revisions remain available for undo/redo until consumed by creation. Missing, consumed or out-of-scope previews return `Invalid artwork preview.` `createFloorPlanObject(input: CreateFloorPlanObjectInput!)` accepts `clubId`, `categoryId`, `name`, `artworkRef`, optional `category` (default `Customizable`, trimmed 1–50 characters), optional `reservable` (default false), and optional `deferResourceCreation` (default false), never raw image data. The modal omits `category` and `reservable`, using the server defaults. Creation consumes the preview and returns `object` and `error`. Decorative objects have `resourceId: null` and appear in Customizable; reservable objects create a linked resource by default and appear in Reservables. Import acceptance passes `deferResourceCreation:true` to save a reusable reservable design with `resourceId:null`; each unbound `bookable:true` placement then creates its own resource when the plan is saved. The flag does not alter authorization, preview ownership, name validation or the library object limit. Artwork remains attached to its placements. Creation does not automatically place an object on the canvas. Generation, refinement and creation require the same staff permission; members and staff from another club cannot call them. Creation records `floor_plan.object_created` in the staff activity log. ### Live artwork progress [#live-artwork-progress] While generation or refinement is pending, a compact elapsed-seconds timer updates every second independently of progress responses. It measures total request time from the mutation's submission timestamp, resets for each attempt (including manual retry), and never resets on stage changes or refetches. Updates calculate the clock difference rather than accumulating interval ticks. The interval is cleared on success, failure or unmount, and the timer disappears when the request settles. Its `aria-live="off"` avoids announcing every second; only stage labels use polite live announcements. The progress steps and debug request show the actual model ID. The loading UI polls `floorPlanArtworkProgress` every second only while the current attempt is pending. Unlike debug traces, this query never serializes or downloads prompts, images, SVG, responses, credentials or private reasoning: ```graphql query ArtworkProgress($clubId: ID!, $categoryId: ID!, $traceId: ID!) { floorPlanArtworkProgress(clubId: $clubId, categoryId: $categoryId, traceId: $traceId) { id status steps { sequence elapsedMs stage model turn } } } ``` Pass the same client-generated UUID `traceId` to generation/refinement. Reads require staff `catalog_manage` permission and match the authenticated user, club, category and trace ID. Missing, expired, evicted or other-owner traces return null. A null result before mutation startup is retried while pending. Polling stops on terminal status, request failure, mutation settlement or unmount; unmount cancels in-flight reads. Missing progress never causes another generation call. `status` is `running`, `succeeded` or `failed` (including cancellation). Ordered steps contain `sequence`, `elapsedMs`, a safe `stage`, optional actual `model` ID or tracer name, and optional editor `turn`. Stages are `preparing`, `generating`, `vectorizing`, `classifying`, `editing`, `inferring-edit`, `rendering`, `inspection`, `correction`, `repair`, `saving`, `completed` and `failed`. Only stages actually reached are emitted, before expensive work; no estimated percentage or simulated timer stages. The UI displays stage history and an indeterminate indicator. Provider failures use the existing mutation toast, never raw provider diagnostics in the loading UI. Progress has its own bounded 100-step buffer, independent of debug payload truncation, with the same 30-minute/process-local retention and eviction limits as debug traces. There are no filters, sorting arguments or pagination; the small history is chronological. ### Artwork debug traces [#artwork-debug-traces] The create-object modal's **Debug** button lists generation and refinement attempts, including failures, independently of preview undo/redo. Each attempt supplies a fresh client-generated UUID as the optional `traceId` argument. Omit it to disable capture. Retrying a completed ID appends a `retry` event without overwriting the earlier failure; simultaneous reuse of a running ID is rejected. Retrieve a trace even when the mutation returns an error or throws a masked GraphQL failure: ```graphql query ArtworkTrace($clubId: ID!, $categoryId: ID!, $traceId: ID!) { floorPlanArtworkTrace(clubId: $clubId, categoryId: $categoryId, traceId: $traceId) { id startedAt status truncated events { sequence elapsedMs stage data } } } ``` All three IDs must be UUIDs. Access requires the same authenticated staff `catalog_manage` permission as generation (club administrators also qualify). The owner is derived from the session, never supplied by the client. Even another administrator cannot retrieve the owner's trace. Missing, expired, evicted, or different-user/category traces return `null`; foreign-club and unauthorized callers are denied before retrieval. Validation and authorization failures before an attempt starts do not create traces. `startedAt` is an ISO timestamp; `status` is `running`, `succeeded`, or `failed`. Events are returned in chronological sequence, with elapsed milliseconds and a stage (`input`, `retry`, `brush-interpretation`, `source`, `vectorization-before`, `vectorization-after`, `request`, `response`, `validation`, `render`, `visual-review`, `artwork`, `result`, or `error`). Vectorization records include the original raster, vector preview, real SVG text, byte size, algorithm and duration; already-vector inputs explicitly report `skipped: true`. Tracing failures have a failed after-event rather than silently disappearing. `data` contains serialized observable data: the actual Sunburst or Flare image prompt and parameters, DeepSeek vision images/evidence/instructions/answer when enabled, Gemini system/user messages and tool schemas, HTTP retries, tool operations, validation repairs, source SVG/strokes, reference images, and results. Provider-private reasoning, encrypted reasoning, credentials and HTTP headers are excluded. SVG is displayed in an image element, never inserted as untrusted SVG markup. Traces are ephemeral and process-local: 30-minute retention, at most eight per user and 24 globally, with oldest-first eviction. Each trace is limited to 100 events and 4,000,000 UTF-16 code units, with 1,500,000 per event. Bounded data is explicitly labelled and sets `truncated`; truncated event data may be plain text rather than valid JSON. Restarting the API loses traces, not artwork previews. There is no cross-replica trace persistence, pagination, sorting or filtering. The modal retains the latest 20 attempt identifiers, fetches traces on inspection, and polls running attempts. Capture is not a public log or durable audit record and does not alter the image generation or Gemini bounded visual-review editing policy. ```graphql mutation GenerateObject( $clubId: ID! $categoryId: ID! $name: String! $generationModel: FloorPlanArtworkGenerationModel! = SUNBURST ) { generateFloorPlanObjectArtwork( clubId: $clubId categoryId: $categoryId name: $name generationModel: $generationModel ) { artwork artworkRef error durationMs } } mutation RefineObject( $clubId: ID! $categoryId: ID! $name: String! $artworkRef: ID! $strokes: [FloorPlanArtworkStrokeInput!]! $classifyBrush: Boolean! = true $thinkingEffort: FloorPlanArtworkThinkingEffort! = LOW $editorModel: FloorPlanArtworkEditorModel! = GEMINI_FLASH ) { refineFloorPlanObjectArtwork( clubId: $clubId categoryId: $categoryId name: $name artworkRef: $artworkRef strokes: $strokes classifyBrush: $classifyBrush thinkingEffort: $thinkingEffort editorModel: $editorModel ) { artwork artworkRef error durationMs } } mutation CreateObject($input: CreateFloorPlanObjectInput!) { createFloorPlanObject(input: $input) { object { id name category reservable resourceId image } error } } ``` ```graphql query LibraryObjects($clubId: ID!, $categoryId: ID!) { floorPlanLibraryObjects(clubId: $clubId, categoryId: $categoryId) { id name category reservable resourceId image } } mutation RenameLibraryObject($clubId: ID!, $categoryId: ID!, $id: ID!, $name: String!) { renameFloorPlanObject(clubId: $clubId, categoryId: $categoryId, id: $id, name: $name) { object { id name category reservable resourceId image } error } } mutation DeleteLibraryObject($clubId: ID!, $categoryId: ID!, $id: ID!) { deleteFloorPlanObject(clubId: $clubId, categoryId: $categoryId, id: $id) { deleted error } } mutation RenameOrdinaryResource($clubId: ID!, $categoryId: ID!, $id: ID!, $name: String!) { renameFloorPlanResource(clubId: $clubId, categoryId: $categoryId, id: $id, name: $name) { resource { id name } error } } ``` Rename requires valid UUIDs and a trimmed name of 1–100 characters without null bytes. It updates linked resource names and matching saved-plan labels. When any placement label changes, the saved plan revision increments once, so an editor still holding the previous revision gets a stale-revision error instead of overwriting the new labels. Deleting a placed custom object removes its placements from the saved plan and increments its revision once; other elements remain. Delete returns `deleted: false` and an `error` for premade/unknown objects. A successful delete removes its private image and associated resource only when it has no bookings, blocked periods, or other placements. Existing bookings and assignments remain attached to a retained resource. Staff assigned to the category can read objects but cannot mutate them; members and other clubs have no access. Deleting a reservation category containing custom library objects is rejected until those objects have been removed through their own deletion mutation, so private images are not orphaned by a database cascade. Each stored `space` and element still accepts an optional `color`, formatted as six-digit hexadecimal (`#RRGGBB`); the back office does not expose this field. Colors are returned by both plan queries and saves. Removing a named plan removes its elements only; reservation resources and bookings remain. The editor asks for confirmation, always shows the main plan, and does not allow deleting the main or final plan. ## Housekeeping [#housekeeping] When a category has `housekeepingEnabled`, resource names in its day and week calendar become buttons, in **Reservations → category** for administrators and **Planning → category** for assigned staff. Staff also get a **Housekeeping** page in their navigation listing every resource of their housekeeping categories with its current state and the checklist items still to do. Clicking a resource in either place opens a side drawer for that resource with separate sections: **Checklist** (hidden when the resource defines none for staff, and each category hidden when empty), **State**, **Consumption** and **History**, listing its ten latest state and consumption entries. The drawer uses the operations below. Members, anonymous callers, staff from other clubs and staff not assigned to the category receive `UNAUTHENTICATED`. ### Checklist [#checklist] Each resource owns an independent checklist. Club administrators and owners configure it with **Edit checklist** in the selected resource's drawer from **Reservations**, using `setResourceChecklist`. Assigned staff can complete tasks but cannot configure them. Reservation category create/update inputs do not contain a checklist. Each item has a client-generated UUID `id`, a `category` and a `label`; returned items also include their owning `resourceId`: * `daily`: resets at the start of every day. * `afterReservation`: resets whenever a reservation of the resource ends, using its [early check-out](#early-check-out) time when recorded, otherwise its scheduled check-out time, otherwise its end time. Canceled reservations are ignored. The mutation takes `SetResourceChecklistInput`: `clubId`, `categoryId`, `resourceId`, and required `items`. It replaces only that resource's checklist in the given order; an empty list clears it. Removed items and their checks are deleted. Editing a reservation category does not change its resources' checklists. Labels are trimmed, must be 1–200 characters without control characters, and unique case-insensitively within a category. A resource holds at most 100 items. IDs must be valid, distinct UUIDs; an existing ID owned by another resource is refused before any write. The resource must belong to the given club/category and housekeeping must be enabled. The result contains `checklist` (including `resourceId`) and nullable `error`. Staff, members, anonymous callers, and administrators outside the club cannot configure checklists. ```graphql mutation SetResourceChecklist($input: SetResourceChecklistInput!) { setResourceChecklist(input: $input) { error checklist { id resourceId category label } } } ``` For example, supply `items: [{ id: "76000000-0000-4000-8000-000000000001", category: "daily", label: "Air the room" }]` alongside the selected club, category, and resource IDs. The same task label in a different room uses a different item ID. No checklist filter, sort, or pagination arguments are exposed; items retain their saved order within each resource. ```graphql query Housekeeping($clubId: ID!, $categoryId: ID!, $dayStart: String!) { housekeeping(clubId: $clubId, categoryId: $categoryId, dayStart: $dayStart) { currency checklist { id resourceId category label } checks { itemId resourceId checkedAt checkedByName } reports { id resourceId reservationId guestName condition note minibar { item quantity } createdAt reportedByName } } } mutation SetHousekeepingCheck( $clubId: ID! $categoryId: ID! $resourceId: ID! $itemId: ID! $checked: Boolean! ) { setHousekeepingCheck( clubId: $clubId categoryId: $categoryId resourceId: $resourceId itemId: $itemId checked: $checked ) { error check { itemId checkedAt checkedByName } } } ``` `dayStart` is the caller's local midnight as an ISO timestamp; the back office currently sends the viewer's own day start. `housekeeping` returns all resource-owned checklist items for the category (filter by `resourceId` to show one resource), the checks still valid in each item's current period, and the 50 most recent reports, newest first. It still returns stored entries after the module is disabled. There is no pagination or filtering. Assigned staff and administrators check or uncheck an item for one resource with `setHousekeepingCheck`. Checking records the current time and the signed-in staff member's name and returns the check; unchecking returns `check: null`. Only the latest check per item and resource is kept. The item must belong to that exact resource, not merely the same category. ### Resource state [#resource-state] The drawer's **Consumption** section reports consumed products: the resource's tracked [resource products](#resource-products-and-reservation-quantities) replace the former minibar inventory, and consumption is billed to a chosen stay with [`reportProductConsumption`](#reporting-product-consumption-from-housekeeping). ```graphql mutation LogHousekeepingReport($input: LogHousekeepingReportInput!) { logHousekeepingReport(input: $input) { error report { id condition note createdAt reportedByName } } } ``` `LogHousekeepingReportInput` takes `clubId`, `categoryId`, `resourceId`, a required `condition` (`clean`, `dirty`, `maintenance` or `outOfService`) and an optional `note` (default `""`). Each report is an immutable log entry; the latest report with a `condition` is the resource's current state. Notes are trimmed and limited to 1,000 characters; line breaks are allowed. Reports are deleted with their resource. `housekeeping.reports` mixes these state reports with the consumption entries recorded by `reportProductConsumption`. A consumption entry has `condition: null`, the consumed product names and quantities in `minibar { item quantity }`, and the stay in `reservationId` and `guestName`; a state report has an empty `minibar` list and null `reservationId` and `guestName`. Invalid input returns `BAD_USER_INPUT`. The mutations return a user-facing `error` with a `null` payload when housekeeping is disabled for the category, when `resourceId` does not belong to the category, or when the task does not belong to the given resource. Checklist configuration errors return an empty `checklist` and an `error` without changing saved items. The author name comes from the staff session, never from the input. ## Customer sheets [#customer-sheets] When a category has `customerSheetsEnabled`, every customer gets one staff-only sheet for that category, shared by all their reservations in it. A sheet holds `preferences`, `contraindications`, a free `note`, and the customer's care `history` in that category. It appears on the reservation page when the reservation is linked to a person, and on the person's page, with one tab per category when the person has several sheets. Guest reservations without a person have no sheet. Members never see sheets. Every staff member of the club can read sheets. Club administrators and owners can always edit them. Other staff can edit a sheet while they are assigned to the resource of one of the customer's reservations in that category, from its creation until one day after it ends (its check-out time when recorded, otherwise its end time); undated requests stay editable. Canceled reservations count. Assignments are evaluated when the sheet is read or saved, so resource staff changes apply immediately. Callers outside the club receive `UNAUTHENTICATED`. ```graphql query CustomerSheets($clubId: ID!, $personId: ID!) { customerSheets(clubId: $clubId, personId: $personId) { personId categoryId categoryName preferences contraindications note updatedAt updatedByName editable history { reservationId startsAt resource attendance } visitCount } } ``` `history` lists the customer's ten latest reservations in the category that have already started, newest first, excluding canceled ones. Each visit gives its `reservationId`, `startsAt`, `resource` name and `attendance`; the back office links each one to its reservation page. `visitCount` counts all of them, beyond the ten listed. The back office shows contraindications as a warning, the three latest visits with an option to show all, and leaves out the reservation being viewed. `customerSheets` returns one sheet per enabled category where the person has a reservation in the club or a saved sheet, ordered by category name. Categories without a saved sheet return empty fields with null `updatedAt` and `updatedByName`. `editable` tells whether the caller may save it. `reservationCustomerSheet(clubId, reservationId)` returns the same fields for the reservation's person and category, or null when the module is disabled, the reservation has no person, or it is not in the club. Neither query accepts filters, sorting or pagination. ```graphql mutation SaveCustomerSheet($input: SaveCustomerSheetInput!) { saveCustomerSheet(input: $input) { error sheet { categoryId preferences contraindications note updatedAt updatedByName editable } } } ``` `SaveCustomerSheetInput` takes `clubId`, `categoryId`, `personId`, `preferences`, `contraindications` and `note`, and replaces the whole sheet. Fields are trimmed and limited to 4000 characters; control characters other than tabs and line breaks are refused. Invalid IDs or fields return `BAD_USER_INPUT`. `error` is `Customer sheet not found.` when the module is disabled or the person has no reservation in the category, and `You can no longer edit this customer sheet.` when the caller's edit window is closed. Each saved change records a `customer_sheet.updated` activity entry on the person with the changed fields, and stores the editor's name and time on the sheet. ## Attendance and member follow-up [#attendance-and-member-follow-up] `markReservationAttendance` accepts an optional `occurrenceId`. Omit it to mark an individual reservation; when supplied, the booking must match that exact occurrence. The required UUID `clubId`, `categoryId`, and `reservationId` always scope the write. Only booked, confirmed and completed reservations can be marked. Pending and canceled bookings return null. Attendance never changes payment, booking status or the scheduled check-in/check-out window. No-shows are marked explicitly; an unmarked booking is never counted as absent. Late is a distinct attendance state and counts as present in the session's attendance total. `late` and `absent` are refused with `BAD_USER_INPUT` "Mark late or absent once the reservation has started." before the reservation's start time, so no no-show can be counted in advance; `present` stays open for early arrivals and `unmarked` clears a mark at any time. The optional `note` is trimmed, limited to 500 characters and stored with that change. Every state change records the previous/new state, authenticated staff identity and time, atomically with the attendance update and webhook. Repeating the current state is a no-op, including its note: it creates neither another event nor another webhook. To correct a mark, choose another state or `unmarked`. The UI toggles a selected state back to unmarked. ```graphql mutation MarkIndividualAttendance($clubId: ID!, $categoryId: ID!, $id: ID!) { markReservationAttendance( clubId: $clubId categoryId: $categoryId reservationId: $id attendance: late note: "Arrived after roll call" ) { reservationId attendance } } query AttendanceHistory($clubId: ID!, $id: ID!, $page: Int! = 1) { reservationAttendanceHistory(clubId: $clubId, id: $id, page: $page) { attendance page hasMore items { id occurredAt actorName previous attendance note } } } query MemberAttendance( $clubId: ID! $personId: ID! $days: Int! = 90 $page: Int! = 1 $attendance: ReservationAttendance ) { personAttendance( clubId: $clubId personId: $personId days: $days page: $page attendance: $attendance ) { present late absent unmarked page hasMore items { id categoryId categoryName startsAt attendance } } } ``` Reservation attendance history uses the same assigned-category staff access as the booking. Member follow-up requires the same club administrator access as the person's profile; planning-only staff and members cannot read it. IDs must be UUIDs. Missing or mismatched records return null. History pages contain up to 20 changes, newest first by time and ID. `page` defaults to 1 and must be between 1 and 1,000,000; `hasMore` controls pagination. There is no client sort. The optional `attendance` filter accepts `present`, `late`, `absent`, or `unmarked`; omitted or null returns all attendance states. It filters rows before pagination and leaves the full-period summary counters unchanged. `days` accepts 30, 90 (default), or 365. The period is the rolling UTC interval from now minus that many days (inclusive) to now (exclusive), applied to reservation start time. Future, undated, pending and canceled reservations are excluded. Summary counters cover the full period and count reservations, not accompanying guests; the four counters are mutually exclusive. Corrections update these current-state totals without erasing history. Rows are paginated by 20 and ordered by start time descending, then ID descending. Times are displayed in the club timezone. Each member row links to its reservation, where the correction history is available. The optional `onlyIfUnmarked` argument of `markReservationAttendance` updates only reservations whose attendance is still `unmarked`. The check happens under the row lock: an existing attendance is returned unchanged, with no new audit event or webhook. The roster uses this guard to mark remaining participants present without overwriting concurrent corrections. A failed batch can be retried; completed changes remain saved. ### No-show sanctions [#no-show-sanctions] Each club can warn, suspend, block bookings or ban members automatically after repeated no-shows. The policy has a `resetAfterDays` counting window (1–3,650 days, default 90) and up to 10 rules. A no-show counts while its reservation started within the last `resetAfterDays` days; older no-shows no longer count, so the counter returns to zero once the window passes without a new one. Only booked, confirmed and completed reservations marked `absent` count. Each rule names a distinct `noShows` threshold (1–100) and an `action`: `warn`, `suspend` or `no_booking` for `suspensionDays` days (1–3,650, starting on the current UTC date), or `ban`. `no_booking` sets the member's status to `no_booking`, which only blocks new reservations. `suspensionDays` is ignored for warnings and bans and returned as null. Rules are returned sorted by threshold. An empty `rules` list disables sanctions. The sanction is applied in the same transaction as `markReservationAttendance` when it marks a member's reservation `absent` and the member's count inside the window becomes exactly a rule's threshold. Marking a reservation older than the window never triggers a sanction. Banned and disabled members are left unchanged. A suspension or booking block never shortens an ongoing one of the same kind: it extends the end date when longer and otherwise leaves it untouched. A suspension replaces an ongoing booking block, but a booking block never replaces an ongoing suspension. The applied status records the reason `"<count> no-shows within <days> days"` and appears in the person's status history with the staff member who marked the no-show. Unmarking or correcting a no-show does not lift a sanction; staff end it from the member profile. Every sanction queues a `club` category [member notification](/en/graphql/notifications) that states the no-show count and the window: "No-show warning" for `warn`, "Club access suspended" and "Bookings blocked" with the end date for `suspend` and `no_booking`, and "Club access revoked" for `ban`. A `warn` rule leaves the member's status unchanged and also reaches suspended members; a sanction that leaves the status unchanged sends nothing. Notifications follow the member's in-app and email preferences for that category, are sent at most once per no-show reservation, and are recorded as `person.notified` activity. Staff with settings access read the policy with `noShowSanctionPolicy(clubId: ID!)` and replace it with `configureNoShowSanctions(clubId: ID!, resetAfterDays: Int!, rules: [NoShowSanctionRuleInput!]!)`. Both return `NoShowSanctionPolicy!`. Out-of-range values, duplicated thresholds, a missing suspension or booking block length or more than 10 rules return `BAD_USER_INPUT`; other callers receive `UNAUTHENTICATED`. Saving an unchanged policy records no activity. ```graphql query NoShowSanctionPolicy($clubId: ID!) { noShowSanctionPolicy(clubId: $clubId) { resetAfterDays rules { noShows action suspensionDays } } } mutation ConfigureNoShowSanctions($clubId: ID!) { configureNoShowSanctions( clubId: $clubId resetAfterDays: 60 rules: [ { noShows: 1, action: warn } { noShows: 2, action: no_booking, suspensionDays: 7 } { noShows: 3, action: suspend, suspensionDays: 14 } { noShows: 5, action: ban } ] ) { resetAfterDays rules { noShows action suspensionDays } } } ``` ## Session cancellation and member notifications [#session-cancellation-and-member-notifications] See [Member notifications](/en/graphql/notifications) for `cancelReservationSession`, notification preferences, the inbox and reminder rules. Session deletion is rejected while active bookings, waitlist entries or unrefunded payments remain. Cancel individual occurrences from their roster before deleting the definition. Confirmations continue using the existing email journal, now respecting the member's reservation email preference. ### Resource calendars and open events [#resource-calendars-and-open-events] The member planning shows scheduled activities together by default, with an activity filter. Cards show the time, activity and registration/open-access status. Selecting a card opens a compact detail dialog with availability, price and one primary action. Session-based categories include collective classes, provider appointments and dated events. Direct booking uses the existing session-based category configuration; the demo category **Cours particulier** retains its conversation-based request examples. Conversation-based undated requests remain an explicit category configuration, not a requirement for coaching. Both session creation inputs accept optional `title` (trimmed, at most 100 characters), `resourceId` (a resource belonging to the same category), and `reservationRequired` (default `true`). Availability occurrences return those fields. A resource-scoped session only accepts that single resource for member bookings, updates and waitlists; resource blocks and overlapping bookings from other occurrences hide conflicting slots. Reservations sharing one occurrence consume its capacity without blocking each other. Staff booking creation rejects a second active reservation for the same person and session occurrence, just like member booking creation. Canceling an inscription allows that person to book the occurrence again. A member can hold only one non-canceled reservation per occurrence; concurrent duplicate requests return `You have already reserved this session.` without creating another booking. This check runs before the capacity check, so a person who already holds a place in a full occurrence gets this message rather than `This session does not have enough places left.` Setting `reservationRequired: false` publishes an open-access event. GraphQL normalizes its capacity to null. Members cannot book or join a waitlist for it. The consumer instead offers an explicit calendar download in the event dialog: an iCalendar file with a stable occurrence UID, UTC times, title and location. This export does not create an internal reservation or subscribe to future event updates. `reservationAvailability.busyPeriods` returns resource IDs and overlapping UTC start/end times for non-canceled bookings. It contains no member, price, message, or reservation ID. Flexible activities display these occupied periods and resource/global blocks in the weekly calendar; a day selection prefills editable start/end dates for a request. An empty day does not guarantee availability or represent published opening hours. No Mews or Book4Time synchronization is provided by this change. ### Member place selection [#member-place-selection] Members can read `reservationFloorPlan(clubId, categoryId)` and `floorPlanLibraryObjects(clubId, categoryId)` for their authenticated club when the reservation category has `visualizationEnabled`. Disabled or unknown categories return null for the plan and an empty object list. Other clubs and anonymous callers remain unauthorized. Saving plans and reading named occupancy remain staff-only. For flexible categories with a plan, the consumer asks for the date and time, then shows the club's placed objects. Members select one resource directly on the plan. Anonymous `reservationAvailability.busyPeriods` and resource/global blocks grey out places overlapping the chosen interval. Selection clears when dates change, and submission is disabled while availability loads or the selected place is unavailable. The existing booking mutation still validates conflicts and membership at submission. The member planning matches `myReservations.reservation.sessionId` plus the start instant and category to each scheduled occurrence. Pending requests show “Request sent”; booked or confirmed reservations show “Already booked”. Selecting either opens the existing reservation. Canceled reservations do not mark the slot as already booked. ### Member reservation editing [#member-reservation-editing] CustomerReservation exposes optional sessionTitle, the scheduled activity title; clients fall back to categoryName when absent. reservationAvailability accepts optional excludeReservationId: ID. Only busy periods belonging to that reservation and to the authenticated member in the queried club/category are omitted. Other members’ reservations remain occupied, and anonymous/staff requests do not apply the exclusion. Session capacity counts are unchanged. Date range validation remains unchanged. Saving an edit still validates conflicts on the server. Basic shapes use solid palette fills. The texture field, enum, pattern renderer and validation have been removed. Seeded palm trees and parasols use the generated `plant` and `umbrella` artwork, preserve their placement, and remain decorative rather than creating reservation resources. Placement and object selection are mutually exclusive. Selecting an existing object cancels placement, and placement previews are hidden over objects. Choosing a library item exits shape editing. Clicking a shape body exits its shape editor; handles remain interactive. Clicking empty canvas clears object selection and returns to the plan palette controls without saving or discarding changes. ## Club credits [#club-credits] See [Club credits](/en/graphql/credits) for credit packs, balances, refill history, credit-priced add-ons and reservations, and cancellation refunds. Membership subscriptions stay billed in money. ### Security-deposit payment state [#security-deposit-payment-state] `Reservation.depositAmount` is the snapshotted deposit in hundredths of a credit. `depositPaid` becomes true only when price and deposit payment commits; `depositRefunded` becomes true when the collected deposit is settled. Both are false when no deposit was collected. Nullable `depositRefundedAmount` is the amount returned when the deposit was settled after departure, and `depositRefundNote` the staff message sent when part of it was kept (see [Departure and deposit refunds](#departure-and-deposit-refunds)). For [external reservations](/en/graphql/external-reservations), amounts are in cents and are paid and refunded by card; `source` is `external` and `externalCustomer` keeps the visitor's contact details after their temporary account is deleted. They are exposed on staff and member detail and list results, including canceled/completed bookings. Paid bookings cannot be reassigned to resources with a different price or deposit. Unpaid resource reassignment updates both snapshots. Waiving a reservation price in a chat proposal does not waive the deposit. The consumer shows the price, deposit, combined total, and deposit state; payment cannot omit the deposit. Cancellation (member, staff, or entire session) refunds both paid components, except that a member cancellation within the [cancellation notice](#cancellation-notice) keeps the price; after departure, staff or the automatic refund return all or part of the deposit to the original member's credit wallet. Refunds retain the original credit name and exact amount, with unique ledger sources preventing repeated or concurrent requests from refunding twice. ### Combining session schedules [#combining-session-schedules] Session-enabled categories display sessions in their calendar; bookings belong to a session's participant list. Creating a booking in these categories requires a `sessionOccurrenceId`. The editor starts with a dated (`custom`) session. Recurrence is optional: add as many `weekly` or `daily` definitions as needed, including several on the same day. Definitions are additive. For example, Monday at 15:00 and 18:00, Friday at 15:00, and daily at 16:00 are four schedules. `ReservationSessionInput` and `ReservationSessionDraftInput` accept `kind: daily` with `startMinutes` (0–1439), `durationMinutes` (1–10080), and an optional capacity. For daily sessions, `weekday`, `startsAt`, and `endsAt` must be omitted/null. Times follow the club's timezone. Daily occurrence IDs use `<definition UUID>:<YYYY-MM-DD>`, as weekly ones do. A dated session uses UTC `startsAt` and `endsAt` and its definition UUID as the occurrence ID. Several dated sessions on the same date are supported, independently of recurring schedules. Ticket verification has a persistent club-wide limit of 100 attempts per minute, including malformed and unknown codes. Staff category restrictions are applied after code lookup; tickets outside those categories return the same invalid result as unknown codes. Session management uses the staff-admin-only `reservationSessionDefinitions(clubId: ID!, categoryId: ID!): [ReservationSessionDefinition!]!` query. It lists the category's stored schedules, including past and distant dated sessions and recurrences with no currently available occurrence. Unlike `reservationAvailability`, it does not apply booking capacity, blocked periods, cancellations or a date window. Both IDs are validated UUIDs; foreign-club categories return an empty list. Members and planning-only staff cannot access this management query. There is no filtering, sorting or pagination; the back office sorts and separates dated and recurring definitions and hides past dated sessions until requested. ```graphql query ManageSessions($clubId: ID!, $categoryId: ID!) { reservationSessionDefinitions(clubId: $clubId, categoryId: $categoryId) { id title kind resourceId reservationRequired weekday startMinutes durationMinutes startsAt endsAt capacity guestPlanIds guestPeople { id name } } } ``` `guestPlanIds` and `guestPeople` describe the [guest list](#session-guest-lists); both are empty for sessions open to every member. A deleted person no longer appears in `guestPeople`, but the restriction remains. For dated definitions, `startsAt` and `endsAt` contain UTC ISO timestamps and recurrence fields are null. Recurring definitions have null timestamps; daily definitions also have null `weekday`. `title`, `resourceId`, and `capacity` are optional. Creation returns to the matching schedule list. Category options only toggle modules for saved categories; management is on the category page. The category tabs occupy their own row. The **Tickets** action appears only for a category with tickets enabled and opens camera/manual verification choices; verification still resolves codes across authorized categories in the selected club. ### Editing one session [#editing-one-session] Administrators can call `updateReservationSession(occurrenceId: ID!, input: ReservationSessionInput!)`. Use an occurrence ID from availability (or a one-off definition ID), the same club/category, `kind: custom`, a resource in that category, and the new UTC `startsAt`/`endsAt`. Title, capacity, access and the guest list are also editable. The result contains `sessionId` and `error`. New sessions created with `createReservationSession` also require a staff-selected `resourceId`. Only future, uncanceled occurrences can be edited. Resource blocks, other bookings and sessions at the target location are checked before any writes. Capacity cannot drop below existing participants; open access cannot replace a session with participants or a waitlist. An invalid scope or business conflict returns a null `sessionId` and an error without changing data. Malformed IDs, dates, missing resources and non-custom input are rejected as `BAD_USER_INPUT`. A one-off retains its definition ID. Editing one recurring occurrence suppresses that original date and creates a one-off, leaving the rest of the series unchanged. Its active bookings, resource assignments and waitlist move atomically to the new session/time/location. Booking IDs, payment amounts, ticket codes and waitlist order are preserved; reservation update events are recorded through the existing notification pipeline. Use the returned `sessionId` for the updated roster and waitlist. The back office exposes **Edit session** on future calendar session details and one-off rows in **Manage sessions**. The edit form uses the club timezone and changes only that occurrence. ## Resource products and reservation quantities [#resource-products-and-reservation-quantities] Each resource can have up to 50 products, configured through `updateReservationResource(input: UpdateReservationResourceInput!)`. Pass `products` to replace its catalog atomically, `[]` to clear it, or omit the field to retain it. Products appear in a compact list; Edit opens one product at a time. Each product has a stable UUID `id`, trimmed `name` (1–100 characters), `amount` in hundredths of club credits (0–100,000,000), required `image`, optional `description` (up to 1,000 characters), and `customerVisible`. Images are PNG or JPEG data URLs, validated by file signature, at most 500 KB each and 5 MB combined. Product IDs and case-insensitive names must be distinct within a resource. Only club administrators can configure the catalog. ```graphql mutation ResourceProducts($input: UpdateReservationResourceInput!) { updateReservationResource(input: $input) { resource { id products { id name amount image description customerVisible } } error } } ``` Resource reads expose `products` through `ReservationResource`. Staff see the whole catalog in their authorized club/type; member responses omit staff-only products at the GraphQL field boundary, including nested resource reads. `trackConsumption` (input default `false`) makes a product available in the housekeeping Consumption tab. It cannot be combined with `customerVisible: true`; such input is rejected. The resource editor presents two billing choices: Reservation add-on (`customerVisible: true`), or Minibar consumption (`customerVisible: false`, `trackConsumption: true`). Staff-only catalog inputs are normalized to minibar consumption; there is no separate staff-extra mode. Minibar products are never offered for advance purchase, including when stale catalog data has both flags set. Products are available with or without housekeeping. Configure minibar items here as staff-only products; record quantities on the guest reservation. There is one catalog and one credit billing path. `createReservation`, `createStaffReservation`, and `updateReservation` accept an optional `products: [{ productId, quantity }]` input. Quantities are integers from 1 to 99, with at most 50 distinct products. Products must belong to the selected resource. Customers can select only customer-visible products; staff can add staff-only consumption such as minibar items. Omit products on update to retain existing lines, or use `[]` to remove them. Remove existing product lines before moving a reservation to another resource. The server calculates the total from resource pricing plus product quantities; clients cannot submit unit prices. `Reservation.products` contains immutable price/name snapshots (`productId`, `resourceId`, `name`, `unitAmount`, `quantity`, `customerVisible`). Catalog repricing and deletion do not alter existing charges. Staff can adjust quantities of existing snapshots, including deleted products. Member responses omit staff-only line details while the reservation total includes all billed products. Catalog and line arrays preserve configured order; there are no separate product filtering, sorting, or pagination arguments. Pending/booked reservations include products in their eventual credit payment. For confirmed/completed reservations, saving changed quantities atomically posts the debit/refund difference to the customer's credit ledger and updates the reservation. Insufficient credit leaves both unchanged. Repeating a save with the same quantities does not charge again. Cancellation refunds the original credit payment plus the net product adjustments once. A free proposal waives both the resource and products by recording zero unit prices. Canceled reservations and reservations with a legacy Stripe checkout cannot have their charged total changed. Product editing is staff-only after submission; members can select quantities when requesting a reservation. ### Reporting product consumption from housekeeping [#reporting-product-consumption-from-housekeeping] `housekeepingConsumption(clubId: ID!, categoryId: ID!, resourceId: ID!)` is restricted to administrators and assigned planning staff. It returns the resource's `products` with `trackConsumption: true` in catalog order, and at most 50 eligible `reservations`, sorted by end time descending and ID descending. Each reservation returns `id`, `guestName`, `startsAt`, `endsAt` and its billed `products` snapshots. Eligible reservations are confirmed or completed, have already started, belong to the selected resource, still have a member, and use credits rather than a legacy Stripe checkout. There are no further filter or pagination arguments. Invalid or disabled housekeeping scopes return empty lists. ```graphql mutation ReportProductConsumption($input: ReportProductConsumptionInput!) { reportProductConsumption(input: $input) { reportId error } } ``` The input requires a new UUID `id`, `clubId`, `categoryId`, `resourceId`, `reservationId`, and a nonempty `products: [{ productId, quantity }]` list. Quantities represent **newly consumed** items, not replacements for previously billed quantities. Up to 50 distinct products are accepted, each quantity 1–99; the accumulated reservation quantity is also limited to 99 per product. Only flagged products belonging to that resource can be reported. Staff choose the stay explicitly; the system does not infer which guest consumed an item from the current room. Prices come from an existing reservation snapshot when present, otherwise from the resource catalog. Credit debits, added quantities and the attributed housekeeping history entry commit in one transaction. Insufficient balance leaves all three unchanged. Reservation cancellation refunds these charges through the existing product adjustment ledger. The report exposes `reservationId` and `guestName` alongside the consumed item names, quantities, author and time. `logHousekeepingReport` remains the condition/note operation. Keep the same request `id` when retrying the same submission: it returns the original `reportId` without adding quantities or debiting again. Reusing that ID with a different reservation, resource or product payload is refused. Invalid IDs or quantities return `BAD_USER_INPUT`; unavailable stays, untracked products and insufficient credits return a user-facing `error` with `reportId: null`. Anonymous callers, customers and unassigned staff cannot read or submit housekeeping consumption. ## People invited to a reservation [#people-invited-to-a-reservation] A reservation can have up to 50 invitees in addition to its booking contact. The booking owner, club administrators, and staff assigned to its category can manage these people. Existing guests and members are linked by normalized email within the same club, both when sending and accepting. The consumer shows the people card directly, including an account-linked indicator and the linked person’s current name. With the default club policy, outside email addresses do not create an account or grant club access. Invitation management does not change the resource booking or price. Only a staff invitation to a full reservation raises `guests` by one, within the session capacity for a session booking (see [Number of people](#number-of-people)). ```graphql query ReservationInvitees($clubId: ID!, $reservationId: ID!) { reservationInvitees(clubId: $clubId, reservationId: $reservationId) { id name email personId status } } mutation InviteToReservation( $clubId: ID! $reservationId: ID! $email: String! $durationHours: Int ) { inviteToReservation( clubId: $clubId reservationId: $reservationId email: $email durationHours: $durationHours ) } mutation RemoveReservationInvitee($clubId: ID!, $reservationId: ID!, $id: ID!) { removeReservationInvitee(clubId: $clubId, reservationId: $reservationId, id: $id) } ``` `clubId` and `reservationId` scope all management operations; `id` identifies the invitee when removing one. Only an email is needed to invite someone. `email` must be valid and is normalized before matching. Duplicate email addresses (including case variants) and the booking contact's email are rejected with `BAD_USER_INPUT`. Canceled, completed, and past reservations cannot receive new invitations. Lists are ordered by creation time then ID, with no filters, sorting arguments, or pagination because each reservation is limited to 50 invitees. `personId` is nullable and `status` is `pending`, `accepted`, or `declined`. Sending returns `true` after the mail service accepts the email. Delivery failure removes the pending invitee and returns a retryable error. Remove an invitee to invalidate their link; removing and inviting again sends a fresh invitation. Invitations do not grant payment controls, tickets, or access to the booking's private conversation. Recipients use the email's `/reservation-invitation?token=…` link without signing in: ```graphql query ReservationInvitation($token: String!) { reservationInvitation(token: $token) { name hostName clubName resource startsAt endsAt status expired requiresAccount hasStoredName } } mutation RespondReservationInvitation($token: String!, $accept: Boolean!, $name: String) { respondReservationInvitation(token: $token, accept: $accept, name: $name) } ``` The random 64-character hexadecimal token is stored only as a hash and is never returned by management queries. Preview returns `null` for unknown or removed tokens. Links expire after seven days or at the reservation's end, whichever comes first; cancellation or completion also makes them unavailable. A pending invitee can accept or decline once; repeated, expired, removed, or canceled responses fail with `BAD_USER_INPUT`. Preview shows the current reservation dates (nullable for an unscheduled request), the club’s IANA timezone, and the response status. A response does not change the booking's confirmation status. Acceptance uses the linked person’s current name automatically. Without an account, it reuses the most recent accepted name for the same normalized email within this club. Preview exposes `hasStoredName` and `name`; recipients with a stored name can accept without entering or submitting one. Otherwise `name` is required on acceptance (trimmed, 1–100 characters). Declining never requires a name. Supplied names do not rename account profiles or override stored names. Unknown guests display their email until a name is available. ### Require guest accounts [#require-guest-accounts] The club admin Settings page offers **Require guests to create an account**, off by default. Members in the club and assigned staff can read the policy; only a club administrator can change it. ```graphql query ReservationGuestPolicy($clubId: ID!) { reservationGuestsRequireAccount(clubId: $clubId) } mutation SetReservationGuestPolicy($clubId: ID!, $required: Boolean!) { setReservationGuestsRequireAccount(clubId: $clubId, required: $required) } ``` Both return a Boolean, with no filters or pagination. Unknown clubs fail with `BAD_USER_INPUT`. The policy is captured when sending an invitation; changing it does not rewrite existing invitations. When enabled, inviting an email without login credentials requires `durationHours` of 12, 24, or 48. The inviter must also satisfy the existing guest-invitation permissions (active non-guest member or administrator). The invitation appears in the consumer Guests tab and uses the existing guest account form. `reservationInvitation.requiresAccount` tells the recipient UI to continue to `/guest-invitation?token=…` with the same token. Direct acceptance through `respondReservationInvitation` fails until an account exists; declining remains available. `acceptGuestInvitation` creates or completes the person’s account, creates the chosen temporary access starting at acceptance, starts their session, and marks the reservation invitation accepted with that person ID, all in one transaction. Removed, declined, expired, completed, and canceled reservation invitations cannot create guest accounts. Delivery failure removes both pending records. People already holding login credentials in the same club use the simple reservation acceptance flow, without a duplicate account, password change, or additional access grant. An account created after sending is detected again on preview and acceptance. Matching never links another club’s person. ### Invitations in the consumer account [#invitations-in-the-consumer-account] The Reservations tab includes an **Invited reservations** section, separate from owned bookings. It shows pending, accepted, and declined invitations, host, resource, dates, timezone, and the booking’s current status. Pending available invitations can be accepted or declined directly. Accepted invitations to a confirmed, upcoming reservation with `createTicket` show a **Download ticket** action that calls `downloadMyReservationTicket` with `reservationId`. ```graphql query MyReservationInvitations($clubId: ID!) { myReservationInvitations(clubId: $clubId) { id reservationId createTicket hostName resource startsAt endsAt timezone status reservationStatus expired } } mutation RespondMyReservationInvitation($clubId: ID!, $id: ID!, $accept: Boolean!) { respondMyReservationInvitation(clubId: $clubId, id: $id, accept: $accept) } ``` Both require member authentication in `clubId` (including active guest accounts). The recipient ID comes from authentication, never a client argument. Invitations match a linked person ID, or the current normalized email if not yet linked, within that club. Existing links survive account email changes. Lists return at most the latest 100 invitations, ordered by creation time then ID descending, without filters or pagination arguments. Removed invitations disappear. The result exposes no invitation tokens, payment data, private conversation, or owner controls. `expired` means the response deadline passed or the booking ended, was canceled, or completed; accepted invitations remain visible after the response deadline. Dates are nullable. The response mutation returns `true` and uses the stored account name. It enforces the same single-response, expiry, booking-state, and account requirements as email-token acceptance. An unrelated invitee ID or club fails without changing the invitation. Viewing an invited reservation does not grant access to the owner’s booking detail or mutations. ### Resource occupant limits [#resource-occupant-limits] `ReservationResource.maxOccupants` is the number of people one booking of the resource can hold, an integer from 1 to 51 that defaults to 1. Set it in the resource editor, for example 2 for a pool lounger or 4 for a hotel suite. `UpdateReservationResourceInput.maxOccupants` keeps the current value when omitted. The limit decides whether bookings ask for a [number of people](#number-of-people) and caps what members can book. Invitations are then limited by the reservation's `guests`, not by the resource: the booking contact counts as one, pending and accepted invitations reserve places, and declined or removed invitations do not. Invitation creation checks this transactionally. The consumer reservation query selects `resources { id name maxOccupants }` and `guests`, shows the occupant count, and disables sending invitations when everyone has been invited. ### Member credit payment summary [#member-credit-payment-summary] `myReservation(clubId: ID!, id: ID!)` also returns nullable `creditPayment` with `balance`, `creditName`, `refundableAmount`, and `refundedAmount`. Amounts are integer hundredths of credits. The summary is available only for the authenticated owner's detail query; list results do not load it. UUID validation and ownership checks are unchanged; another member's reservation returns `null`. `refundableAmount` is the remaining net debit recorded for the reservation, product adjustments, and deposit after previous refunds. `refundedAmount` is the actual reservation and deposit refunds recorded in the credit ledger. Unpaid or free reservations have no credit refund; externally paid amounts are not credit refunds. For a reservation paid by card through Stripe Checkout, `Reservation.cardRefundedAmount` is the amount Stripe refunded to the card, in cents: the price on a full refund plus the deposit once it is refunded (only the deposit after a late cancellation). It is null while nothing was refunded by card, and always null for credit payments. The member site shows it on the canceled reservation as `{amount} refunded to your payment method.` and keeps "No credits were debited" for reservations payable in credits. The balance is informational; payment always checks the current balance atomically. ```graphql query ReservationPayment($clubId: ID!, $id: ID!) { myReservation(clubId: $clubId, id: $id) { creditPayment { balance creditName refundableAmount refundedAmount } } } ``` ### Retrying a member reservation request [#retrying-a-member-reservation-request] `CreateReservationInput.requestId` is an optional UUID, scoped to the authenticated member and club. Reuse it with the same category, resource, dates, session occurrence and products when retrying `createReservation` after an uncertain network response. Concurrent retries return the same reservation, including its current status, without repeating creation side effects. Changing the payload with the same UUID returns a validation error. Use a new UUID for a new request; requests without a UUID do not have this retry guarantee. Invalid UUIDs are rejected before creation. The consumer form keeps the same UUID for retries of the same submitted values while that form remains open. Starting another request generates a new UUID. This guarantee does not deduplicate separately opened forms or browser reloads. Accepting a time proposal rechecks resource availability and rejects proposals whose start time has passed, leaving the request pending for another proposal. ### Decline or withdraw a replacement proposal [#decline-or-withdraw-a-replacement-proposal] `declineReservationProposal(clubId: ID!, id: ID!, proposalVersion: ID!): ReservationResult!` lets the authenticated member keep their accepted booking. `withdrawReservationProposal` has the same arguments and requires staff `reservations_manage` permission in the club. All IDs must be UUIDs. Both return `reservation` and `error`, like proposal acceptance. These actions only apply to a booked reservation with an outstanding replacement proposal, including an expired proposal. The exact displayed `proposalVersion` is checked under the reservation lock; a stale version, another member's reservation or a completed acceptance cannot be dismissed. The accepted dates, resource, price and deposit remain unchanged. Only the proposal is cleared. `paymentVersion` changes, so clients refresh the reservation before offering payment again. Declines and withdrawals are recorded in activity history. ```graphql mutation KeepAcceptedTime($clubId: ID!, $id: ID!, $proposalVersion: ID!) { declineReservationProposal(clubId: $clubId, id: $id, proposalVersion: $proposalVersion) { reservation { id startsAt endsAt amount proposalVersion paymentVersion } error errorCode } } ``` ### Coach profiles in conversations [#coach-profiles-in-conversations] `ReservationMessage.coach` is a nullable profile containing `id`, `name`, `bio` and `avatar`. Professional messages record the authenticated sender's staff ID; clients cannot choose the sender. Profiles show the latest saved name, biography and photo, not a snapshot taken when the message was sent. ```graphql query ConversationCoach($clubId: ID!, $reservationId: ID!) { myReservationChat(clubId: $clubId, reservationId: $reservationId) { id messages { id author senderName coach { qualifications { id kind title issuer issuedOn expiresOn } id name bio avatar } body createdAt } } } ``` The member must own the reservation in the authenticated club. There is no public staff directory or unscoped coach lookup. `coach` is null for customer messages, legacy messages without a sender ID, deleted staff accounts, or staff whose club access has been removed. `senderName` remains available in these cases. Profiles exclude email addresses, account permissions and staff documents. The field is also available on messages returned by the staff conversation query and message mutations. Existing conversation ordering and pagination behavior are unchanged. ### Coach qualifications [#coach-qualifications] `CoachProfile.qualifications` lists diploma and professional-card metadata (`id`, `kind`, `title`, `issuer`, `issuedOn`, `expiresOn`) from the staff dossier in the reservation club's space. The coach must still belong to that space and have access to the club. Dates are ISO calendar dates or null. Expired records retain their expiry date; inclusion does not mean a document has been verified. Other supporting documents, registration numbers, filenames, storage keys and original files are not exposed to members. Access remains limited to participants in the member's own reservation conversation. ### Layer order [#layer-order] The staff plan editor exposes **Send to back**, **Send backward**, **Bring forward** and **Bring to front** for a single selected object. Controls are disabled for locked objects, pending saves and stack boundaries. Each move is one undoable edit; other spaces, geometry and booking bindings are preserved. The object list shows frontmost items first; `elements` is saved in the opposite, back-to-front draw order. The consumer renders that same persisted order. Agent example: ```json { "expected_version": 3, "operations": [{ "op": "move_layer", "id": "existing-instance-id", "direction": "back" }] } ``` # Webhook endpoints Manage a club's outbound notification endpoints. Source: https://docs.champagne.dev-ukab.fr/en/graphql/webhooks Only a staff account with the `admin` role **for the requested club**, or the `settings_manage` permission, can use these operations. Member sessions, planning staff, anonymous requests and administrators of a different club are denied. The Settings page uses the same contract. The [delivery contract](/en/webhooks) describes event types, signing, retries and the embedded pg-boss consumers. A club registers up to **10 endpoints**. Each endpoint has its own URL, event selection, signing secret and paused state, and receives its own copy of every event it subscribes to. ## List endpoints [#list-endpoints] ```graphql query ClubWebhookEndpoints($clubId: ID!) { clubWebhookEndpoints(clubId: $clubId) { id clubId url enabled enabledEvents createdAt updatedAt } } ``` `clubId` must be a UUID in the authenticated administrator's club scope. Endpoints are returned oldest first; a club without endpoints returns an empty list. `enabled` is false while the endpoint is paused. `enabledEvents` lists selected event type IDs. The query never returns a signing secret. There are no filters, caller-selected sorting or pagination. ## Available event types [#available-event-types] ```graphql query ClubWebhookEventCatalog($clubId: ID!) { clubWebhookEventCatalog(clubId: $clubId) { type title description } } ``` The global `webhook_event_catalog` database table is seeded at API startup with currently implemented notification types. Settings renders its entries and descriptions in a multi-select. Entries are returned in display order; there are no filters, pagination or caller-selected sorting. Synthetic `webhook.test` is not in the catalog and cannot be selected. `clubId` must be a UUID in the administrator's club scope even though the catalog contents are global. ## Add an endpoint [#add-an-endpoint] ```graphql mutation CreateClubWebhookEndpoint($clubId: ID!, $url: String!, $enabledEvents: [String!]) { createClubWebhookEndpoint(clubId: $clubId, url: $url, enabledEvents: $enabledEvents) { endpoint { id url enabled enabledEvents } signingSecret } } ``` Pass a public HTTPS URL with a DNS hostname (no IP literal), no embedded credentials and no fragment. Pass `enabledEvents` as a list of `type` IDs from the global catalog; omitting it selects every implemented event, and an empty list queues nothing. The endpoint starts enabled. The response includes a random `signingSecret` generated for this endpoint. It is stored in the private PostgreSQL database and is visible to database administrators; protect database access and backups. `BAD_USER_INPUT` is returned, without saving anything, for an invalid URL, an unknown club, a URL the club already registered, duplicate or unknown event IDs (including `webhook.test`), or an eleventh endpoint. ## Change or pause an endpoint [#change-or-pause-an-endpoint] ```graphql mutation UpdateClubWebhookEndpoint( $clubId: ID! $endpointId: ID! $url: String $enabledEvents: [String!] $enabled: Boolean ) { updateClubWebhookEndpoint( clubId: $clubId endpointId: $endpointId url: $url enabledEvents: $enabledEvents enabled: $enabled ) { id url enabled enabledEvents updatedAt } } ``` Every argument besides the two IDs is optional; an omitted or null argument keeps the current value. `url` and `enabledEvents` follow the rules of creation, and a URL edit keeps the signing secret. Pass `enabled: false` to pause the endpoint and `enabled: true` to resume it. Events are queued for an endpoint only while it is enabled **and** their type is selected. Pausing skips its pending deliveries, including retries; deselecting a type skips the pending deliveries of that type. Resuming or reselecting does not forward events that happened meanwhile. Other endpoints of the club are unaffected. An endpoint that does not belong to the club returns `BAD_USER_INPUT`. ## Delete an endpoint [#delete-an-endpoint] ```graphql mutation DeleteClubWebhookEndpoint($clubId: ID!, $endpointId: ID!) { deleteClubWebhookEndpoint(clubId: $clubId, endpointId: $endpointId) } ``` Returns `true` once the endpoint and its signing secret are removed. Its pending deliveries are skipped. An endpoint that does not belong to the club returns `BAD_USER_INPUT`. ## Reveal signing secret [#reveal-signing-secret] ```graphql mutation RevealClubWebhookSecret($clubId: ID!, $endpointId: ID!) { revealClubWebhookSecret(clubId: $clubId, endpointId: $endpointId) { signingSecret } } ``` An administrator of the specified club can explicitly retrieve the secret of one of its endpoints; each reveal is recorded in the club activity. An endpoint outside the club or an invalid UUID returns `BAD_USER_INPUT`. The list query never includes secret material. The Settings screen keeps the value hidden until the administrator requests it. ## Reset signing secret [#reset-signing-secret] ```graphql mutation RotateClubWebhookSecret($clubId: ID!, $endpointId: ID!) { rotateClubWebhookSecret(clubId: $clubId, endpointId: $endpointId) { endpoint { id url updatedAt } signingSecret } } ``` Returns a fresh secret for that endpoint only; the other endpoints keep theirs. An endpoint outside the club returns `BAD_USER_INPUT`. Update the receiver to accept the new secret; in-flight and retried events use the active secret at delivery time. Do not put the signing secret in logs or a URL. The local GraphQL debugger redacts `signingSecret` fields in mutation responses; production logs must also remain private. ## Send a test delivery [#send-a-test-delivery] ```graphql mutation SendClubTestWebhook($clubId: ID!, $endpointId: ID!) { sendClubTestWebhook(clubId: $clubId, endpointId: $endpointId) { eventId error } } ``` This mutation requires the same scope and UUID validation as the other operations. It sends a real signed HTTPS POST to the endpoint's **saved** URL using its current signing secret, whether or not the endpoint is paused. The synthetic `webhook.test` payload contains `id`, `type`, `occurredAt`, `clubId`, and `data: { synthetic: true }`, but no member data. It bypasses the outbox and event selection, so it does not retry or generate a business event. A 2xx response returns `eventId` and null `error`; an endpoint outside the club or a failed delivery (a non-2xx response, a timeout or an unreachable endpoint) returns null `eventId` and a generic `error` without exposing the URL or signing secret. This `error` is an answer about the club's endpoint, not an API failure; the back office shows it as such. There are no filters, sorting, or pagination. # Overview Step-by-step help for club staff, members and club admins. Source: https://docs.champagne.dev-ukab.fr/en/guide This guide shows how to use Champagne day to day. Each page follows one task, names the buttons and menus exactly as they appear on screen, and shows a screenshot of the real interface filled with example data. ## Find your part [#find-your-part] [Club staff](https://docs.champagne.dev-ukab.fr/en/guide/staff/first-day) You work at the club: you welcome members, handle reservations and answer messages in the back office. [Members](https://docs.champagne.dev-ukab.fr/en/guide/members/get-started) You belong to a club or want to join one: you apply, book and manage your membership from the member app. [Club admins](https://docs.champagne.dev-ukab.fr/en/guide/admins/set-up-your-club) You run the club: you set up its plans, forms, website and payments, and invite your team. ## Words used in this guide [#words-used-in-this-guide] | Word | Meaning | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | <span id="back-office">Back office</span> | The website club staff use to run the club. | | <span id="member-app">Member app</span> | The website members use to apply, book and follow their membership. | | <span id="plan">Plan</span> | Something your club sells, such as a monthly membership or a pack of credits, with its price. | | <span id="credits">Credits</span> | A balance that members spend on reservations and add-ons. Members buy credits in packs. | | <span id="application">Application</span> | The form someone fills in and signs to join a club. Staff review it before the person becomes a member. | | <span id="reservation-category">Reservation category</span> | A family of things members can book, such as tables, rooms or coaching sessions. | | <span id="role">Staff role</span> | What a staff member is allowed to see and do in the back office. | > Screenshots come from a demonstration club called Ukab Clubs. The people, bookings and amounts you > see are invented examples. # Club settings Set your club's address and time zone, member fields, reservation rules and booking categories. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/club-settings The **Settings** section holds the rules that apply to the whole club. It is organized in five tabs: **Club**, **Members**, **Reservations**, **Payments** and **Integrations**. Each card saves on its own: change a value, then select the **Save** button of that card. Settings apply to the club selected at the top left. If you manage several clubs, check its name before you change anything. ## 1. Set the club's address and time zone [#1-set-the-clubs-address-and-time-zone] In the left menu, select **Settings**. The **Club** tab opens first. <figure> <img src="/images/guide/admins/settings-club.png" alt="Club tab of the settings with a Hosted club address card showing the ukab-clubs subdomain, a Club address card filled with 51 rue de la Démonstration, Paris 75008, France, and the start of the Club time zone card." width="1280" height="800" /> <figcaption> The Club tab of the example club Ukab Clubs. </figcaption> </figure> | Card | What it sets | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Hosted club address** | The web address of your club's own sign-up site. See [Your club's website](/en/guide/admins/hosted-site). | | **Club address** | The address shown to members and applicants with the club's details. | | **Club time zone** | The time zone used for opening hours, reservations and reports. Type part of a city name in **Time zone** to find it fast. | ## 2. Define member attributes and tags [#2-define-member-attributes-and-tags] The **Members** tab has two cards. * **Member attributes** are extra fields kept on every member's profile, such as a preferred language or a profession. Answers given in an [application](/en/guide#application) form can fill them in automatically. * **Member tags** are labels for your team only, used to group people and filter the **People** list. Members never see them. <figure> <img src="/images/guide/admins/settings-members.png" alt="Members tab with three member attributes, Preferred language (Choice), Printed newsletter (Yes / no) and Profession (Text), each with edit and delete buttons, and a Member tags card with Founding member (3 people), Juniors (3 people) and Tournament players (4 people), each with edit and delete buttons, and an Add a tag button." width="1280" height="800" /> <figcaption> The three example attributes and three example tags of Ukab Clubs. The buttons to use are outlined in orange. </figcaption> </figure> To add an attribute: 1. Select **Add attribute**. 2. Enter its **Label**, for example "Shoe size". A **Key** is suggested from the label. 3. Choose its **Type**: **Text**, **Number**, **Date**, **Yes / no** or **Choice**. For a choice, enter at least two options with **Add option**. 4. Select **Save**. To add a tag, select **Add a tag**, enter its **Label** and select **Save**. Each tag shows how many people carry it. Use the pencil button to rename an attribute or a tag, and the bin button to delete it. > Deleting an attribute deletes every member's value for it. Deleting a tag removes it from every > person who carries it. ## 3. Set the reservation rules [#3-set-the-reservation-rules] The **Reservations** tab holds the rules that apply to every [reservation category](/en/guide#reservation-category). | Card | What it does | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Reservation guests** | Turn on **Require guests to create an account** if the people a member invites must have an account. The switch applies as soon as you flip it. | | **Security deposits** | **Refund automatically after**: deposits your team has not refunded are returned in full after this number of days. | | **No-show sanctions** | Sanctions applied automatically to members who book and do not come. | To add a no-show sanction: 1. Set **Counter resets after** and select the **Save** button next to it: a member's no-shows are forgotten after this number of days. 2. Select **Add rule**. 3. Enter the number of **No-shows** that triggers the rule. 4. Choose the **Sanction**: **Warn**, **Suspend**, **Block bookings** or **Ban**. For a suspension or a booking block, enter the number of **Days** it lasts. 5. Select **Save**. <figure> <img src="/images/guide/admins/settings-no-show.png" alt="Reservations tab with the Require guests to create an account switch off, Refund automatically after set to 7 days, Counter resets after set to 90 days and a new rule being added: 3 no-shows, Suspend, 7 days, with Cancel and Save buttons, Save outlined in orange." width="1280" height="800" /> <figcaption> An example rule that suspends a member for 7 days after 3 no-shows. </figcaption> </figure> Each rule then appears as a card, such as **3 no-shows** with **Suspended for 7 days**. Add up to ten rules to make sanctions stricter as no-shows add up. Use the pencil button to change a rule and the bin button to remove it. ## 4. Set up what members can book [#4-set-up-what-members-can-book] Reservation categories, such as a restaurant, a room or a group class, are set up from the **Reservations** section. Each thing that can be booked in a category, such as a table or a coach, is a resource. * To create a category, select the **+** button at the end of the category strip. * To change a category, open it and select **Category settings**. <figure> <img src="/images/guide/admins/category-settings.png" alt="Category settings dialog of Le Restaurant on the Booking tab: How members book set to A party size and a time, the Waitlist switch on, the Non-members switch greyed out, and Delete category, Cancel and Save buttons." width="1280" height="800" /> <figcaption> The booking settings of the example category Le Restaurant. </figcaption> </figure> The **Category settings** dialog has five tabs: **General**, **Resources**, **Booking**, **Hours** and **On site**. Booking rules and opening hours are explained step by step in [Set up reservation categories](/en/guide/admins/reservation-categories), prices and products in [Resources, prices and products](/en/guide/admins/resources-and-prices), and sessions in [Sessions, guest lists and blocked periods](/en/guide/admins/sessions-and-blocked-periods). To draw the plan of a space, see [Floor plan editor](/en/guide/admins/floor-plan-editor). ## 5. Other settings [#5-other-settings] | Where | Card | What it is for | | --------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Payments** tab | **Stripe merchant** | Connects the club to online payments. See [Payments](/en/guide/admins/payments). | | **Payments** tab | **Club credits** | Names your credits and sets their value. See [Payments](/en/guide/admins/payments). | | **Integrations** tab | **Newsletter sending** | The **Sending domain**, **Sender name** and **Sender address** of your newsletters. See below. | | **Integrations** tab | **Webhook endpoints** | For your technical provider: sends club events to other systems. See below. | | **Integrations** tab | **API tokens** | For your technical provider: lets a tool such as Terraform manage the club's configuration. Only administrators see it. Each token gets its own **Permissions** (the areas it reaches), one **Access** level, an **Expiration date (optional)** and, with **Hide personal data**, reads guest names anonymized, and is shown once, when you create it. The **⋯** menu on its row offers **Block**, **Unblock** and **Delete**. | | **Report a bug**, top right | - | Describe a problem to the Champagne team: **Title**, **What happened?** and **What were you doing?**, then **Submit bug report**. Do not include passwords. | ### Send newsletters from your own address [#send-newsletters-from-your-own-address] To send newsletters from your own address, open the **Integrations** tab, enter a **Sending domain** reserved for newsletters, a **Sender name** and a **Sender address**, and select **Set up**. Select **Show DNS records** to see the records to add: give them to whoever manages your domain name. Once they are added, select **Check again**. The card shows **Waiting for DNS** until the records are found, then **Verified**. Your team can then send newsletters as described in [Newsletters](/en/guide/staff/newsletters). ### Connect other systems with webhooks [#connect-other-systems-with-webhooks] A webhook lets Champagne notify another system, such as your own website, each time something happens in your club. Your technical provider gives you the address to use. You can connect up to 10 systems, each with its own address, events and signing secret. 1. On the **Integrations** tab, under **Webhook endpoints**, select **Add endpoint**. Enter the **Webhook URL**, choose the **Events to send** and select **Save**. The endpoint appears in the list with its signing secret: your provider uses it to check that messages really come from Champagne. 2. Open the **⋯** menu of an endpoint and select **Reveal secret** to show its secret again and pass it on to your provider, then **Hide secret**. Each reveal is recorded in your activity history. 3. In the same menu, select **Send a test**. **Test webhook delivered.** means the other system received it. **Test webhook not delivered** means the other system did not accept it: the message gives the reason to pass on to your provider. The same menu also offers **Edit**, to change the address or the events, **Pause**, to stop sending to that system until you select **Resume**, and **Delete**. Events that happen while an endpoint is paused are not sent later. If a secret may have been shared by mistake, select **Reset secret** in the menu of that endpoint and confirm in the **Reset signing secret?** window. The previous secret stops working at once: give the new one to your provider. The other endpoints keep their secret. # Floor plan editor Draw the plan of a space by hand, link each table or lounger to a bookable resource, and save the layout. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/floor-plan-editor A floor plan shows a space as it really is, such as a pool deck with its loungers or a dining room with its tables. Your team uses it to see at a glance what is free and to book a spot, as described in [Use the club calendar](/en/guide/staff/planning). This page shows how to build and change the plan by hand. To draw new objects with AI or to rebuild a plan from a picture, see [AI tools for your floor plan](/en/ai-flows). ## 1. Open the editor [#1-open-the-editor] 1. Turn on the **Floor plan** switch of the category, on the **Resources** tab of its **Category settings**. See [Set up reservation categories](/en/guide/admins/reservation-categories). 2. In **Reservations**, select the category. The plan appears under the calendar, below **Floor plan**. 3. Select **Edit floor plan**. <figure> <img src="/images/guide/admins/floor-plan-editor.png" alt="Floor plan editor of the Piscine extérieure plan: the Object library on the left with Reservables shapes such as Rectangle, Circle and L-shape, the pool with five loungers, two plants and a parasol in the middle, Transat #3 selected, and on the right its settings: layer buttons, Width 76, Height 145, Keep aspect ratio, Chairs 0, Create or assign to resource set to Transat #3, and Edit shape. Above, the Add plan, Import plan, Undo, Redo, zoom, Discard changes and Save plan buttons." width="1280" height="800" /> <figcaption> Editing the example pool plan of Place à la piscine, with the lounger Transat #3 selected. The numbers match the three parts listed below. </figcaption> </figure> The editor has three parts: 1. the **Object library**, on the left, with the objects you can place; 2. the plan itself, in the middle; 3. on the right, the settings of the selected object or of the plan, and **Objects in this plan**, the list of everything on it. Nothing is kept until you select **Save plan**. **Discard changes** drops everything since your last save. Use **Undo** and **Redo** to step back and forth. ## 2. Place objects [#2-place-objects] The **Object library** has two tabs: * **Reservables**: shapes that stand for something people book, such as a table, a lounger or a room: **Rectangle**, **Circle**, **Triangle**, **L-shape**, **Hexagon**, **Semicircle**, **U-shape** and **Freeform polygon**. * **Cosmetic**: decorative objects, such as plants, doors and other scenery. <figure> <img src="/images/guide/admins/floor-plan-library.png" alt="Object library on the Reservables tab with the Rectangle shape outlined in orange and the Circle shape next to it, beside the pool plan and its Plan settings." width="1044" height="258" /> <figcaption> The Object library of the example pool plan. The control to use is outlined in orange. </figcaption> </figure> To place an object, drag it from the library onto the plan. You can also select it in the library, then click the spot on the plan. Press Escape to cancel. A plan holds up to 200 objects. <figure> <video src="/videos/guide/admins/floor-plan-drag.webm" aria-label="The Rectangle shape is dragged from the Object library onto the pool plan, where it appears selected with its settings on the right." width="882" height="640" /> <figcaption> Dragging a rectangle onto the example pool plan. </figcaption> </figure> ## 3. Move and arrange objects [#3-move-and-arrange-objects] * Select an object by clicking it on the plan or in **Objects in this plan**. Its name and settings appear on the right. * Move it by dragging it. * Rotate it with the handle at its top right corner, and resize it with the handle at its bottom right corner. * **Width** and **Height** change the size of a reservable shape. Untick **Keep aspect ratio** to change one without the other. Decorative objects have a **Scale** slider. * The four arrow buttons set the **Layer order**: **Send to back**, **Send backward**, **Bring forward** and **Bring to front**. Use them when objects overlap. The buttons next to the object's name **Duplicate selection**, **Lock or unlock selection** and **Remove from plan**. A locked object cannot be moved or changed. If you remove a selection that includes locked objects, Champagne asks **Remove locked objects?**: select **Remove from plan** to confirm. | Shortcut | Action | | ----------------------- | ------------------------ | | Delete or Backspace | Remove the selection. | | Escape | Clear the selection. | | Ctrl+Z (Cmd+Z on a Mac) | Undo. | | Ctrl+Shift+Z or Ctrl+Y | Redo. | | Ctrl+D (Cmd+D on a Mac) | Duplicate the selection. | To look around, use the **Zoom in** and **Zoom out** buttons above the plan, or hold Shift and turn the mouse wheel. To move around, hold Shift and drag the plan, or drag with the middle mouse button. Dragging an empty part of the plan without Shift selects every object inside the box you draw. **Reset view** fits the whole plan on screen. ## 4. Link an object to a resource [#4-link-an-object-to-a-resource] A reservable shape can be booked once it is linked to a resource of the category. Select it, then choose in **Create or assign to resource**: * an existing resource, such as Transat #3; * **Create a new resource**: a new resource is created when the plan is saved; * **No booking binding**: the shape stays on the plan without being bookable. In a category with a floor plan, resources are added and removed this way, from the plan. <figure> <img src="/images/guide/admins/floor-plan-object.png" alt="Settings of the lounger Transat #3 with 0 Chairs and the Create or assign to resource menu set to Transat #3, outlined in orange." width="240" height="172" /> <figcaption> The lounger Transat #3, linked to the resource of the same name. </figcaption> </figure> ## 5. Add chairs to a table [#5-add-chairs-to-a-table] For a table, set the number of **Chairs** around the shape, up to 16. The chairs are placed around the edge. To place them yourself, drag a chair along the edge of the selected shape. ## 6. Change the outline of a shape [#6-change-the-outline-of-a-shape] To give a shape its exact outline, select it and select **Edit shape**. <figure> <img src="/images/guide/admins/floor-plan-shape.png" alt="The same pool plan zoomed in, with Transat #3 in shape-editing mode showing square points at its corners, round handles and plus buttons on its sides, and Edit shape highlighted on the right with the hint about dragging points and rounding corners." width="1280" height="800" /> <figcaption> Reshaping the example lounger Transat #3. The control to use is outlined in orange. </figcaption> </figure> * Drag a point to move it. * Select a **+** button on a side to add a point there. * Pull a round handle to round a corner. * Double-click a point to add a bulge. Press Delete to remove the selected point. Select **Edit shape** again when you are done. **Reset shape** brings back the original outline. It keeps the position, size and rotation. ## 7. Manage several plans [#7-manage-several-plans] A category can have several plans, for example one per floor or a terrace and an indoor room. Their names are shown above the editor, with the number of objects on each. * Select **Add plan** to create one, and select a plan's name to open it. * With nothing selected, the right side shows **Plan settings**: the **Plan name** and its **Palette** of colors, such as **Lagoon**, **Terracotta**, **Grove** or **Dusk**. * **Delete plan** removes a plan and its objects. Reservations are kept. The first plan cannot be deleted. <figure> <img src="/images/guide/admins/floor-plan-plans.png" alt="Plan bar with the Piscine extérieure plan and its 9 objects, an Add plan button outlined in orange and Import plan, above the editor, with Plan settings on the right: Plan name, Palette and a Delete plan button." width="1044" height="343" /> <figcaption> The single plan of the example pool category, with its settings. </figcaption> </figure> ## 8. Save the plan [#8-save-the-plan] Select **Save plan**. The plan is then shown to your team in the **Club calendar**, with each linked object showing whether it is free. Each save is recorded in the **History** of the colleague who made it. See [Manage your team](/en/guide/admins/manage-your-team). # Your club's website Publish a sign-up site in your club's colors, at your club's own web address. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/hosted-site Your club can have its own [application](/en/guide#application) website. Visitors open it from a link you share, for example on your website or social media, and apply to join your club. The site shows your club's name, your color and your font, with a small **Powered by Champagne** mention. Setting it up takes two steps: choose the site's address once, then publish each application form you want to offer there. ## 1. Choose your club's address [#1-choose-your-clubs-address] In the left menu, select **Settings**. On the **Club** tab, find the **Hosted club address** card. <figure> <img src="/images/guide/admins/hosted-address.png" alt="Hosted club address card with the subdomain ukab-clubs followed by the Champagne address, and a Save button." width="1044" height="116" /> <figcaption> The example address of Ukab Clubs. On your screen, the end of the address shows your Champagne domain. The field to use is outlined in orange. </figcaption> </figure> 1. In **Subdomain**, type a short name for your club, such as `ukab-clubs`. Capitals become lowercase, accents are removed, and spaces and other signs become hyphens. 2. Select **Save**. Your club's site then lives at this subdomain, followed by the Champagne address shown next to the field. The same address is used for conversations about reservations. > If you change the subdomain, links you already shared with the old address stop leading to your > site. Update them wherever you published them. ## 2. Publish an application form on the site [#2-publish-an-application-form-on-the-site] Each application form, called an onboarding, is published separately. In the left menu, select **Applications**, then **Manage onboardings**, and select **Edit onboarding** on the form to publish. Building the form itself is explained in [Application forms](/en/guide/admins/onboarding-forms). <figure> <img src="/images/guide/admins/hosted-settings.png" alt="Editing the Founding members onboarding: the Hosted application settings card has its publish switch on, the Font list open with Modern, Classic and Editorial, a primary color of #2563EB, and a note saying the form is available from the club's hosted address at /onboarding/founding-members after saving." width="1280" height="800" /> <figcaption> The hosted settings of the example onboarding Founding members, with the font list open. </figcaption> </figure> In the **Hosted application settings** card: 1. Turn on the switch at the top right of the card to publish the form. 2. Choose a **Font**: **Modern**, **Classic** or **Editorial**. Each option is shown in its own style. 3. Set the **Primary color** of the site. Type a color code such as `#2563EB`, or select the pipette button to pick a color. 4. Select **Save onboarding**. The bottom of the card shows the path of the form, such as `/onboarding/founding-members`. Add it after your club's address to get the link to share. To take a form off the site, turn the switch off and select **Save onboarding**. ## 3. What visitors see [#3-what-visitors-see] The link opens a welcome page with the form's name and description, in your color and font. <figure> <img src="/images/guide/admins/hosted-entry.png" alt="Hosted site of Ukab Clubs in blue: the Founding members form with its description, a Start application button and a Continue an application button, with Powered by Champagne at the top right." width="1280" height="800" /> <figcaption> The example Founding members form, as visitors see it on the Ukab Clubs site. </figcaption> </figure> * **Start application** opens the form, step by step. * **Continue an application** lets someone who already started come back to it. They enter their email address and receive a code. <figure> <img src="/images/guide/admins/hosted-application.png" alt="First step of the Join Ukab Clubs application on the hosted site, Step 1 of 4, with First name and Last name fields and a Continue button." width="1280" height="800" /> <figcaption> The first step of the example application on the Ukab Clubs site. </figcaption> </figure> <figure> <img src="/images/guide/admins/hosted-continue.png" alt="Find my application card on the hosted site with an Email address field and a Send code button." width="448" height="334" /> <figcaption> The page behind Continue an application. </figcaption> </figure> Emails about the application, payment and signature pages bring applicants back to your club's site. Once they have completed every step after approval, members sign in to their account on the Champagne [member app](/en/guide#member-app): the site shows them a **Sign in to my account** link. ### When the site shows "Application site unavailable" [#when-the-site-shows-application-site-unavailable] Visitors see this page when the link does not lead to a published form of your club. <figure> <img src="/images/guide/admins/hosted-unavailable.png" alt="White page with the title Application site unavailable and the line Check the address or contact the club." width="576" height="144" /> <figcaption> The page shown for an example link to a form that does not exist. </figcaption> </figure> Check three things: * the link uses your club's current subdomain, as set in **Hosted club address**; * the path after `/onboarding/` matches the one shown at the bottom of the form's **Hosted application settings** card; * the form's publish switch is on, and the form was saved. New applications arrive in **Applications**, like any other. See [Applications](/en/guide/staff/applications). # Invite your team Give a colleague access to the back office and choose what they can see and do. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/invite-your-team Each person on your team signs in with their own staff account. You create it by sending an invitation from the **Team** section. You choose which clubs they work for and what they are allowed to do there. ## 1. Open the Team section [#1-open-the-team-section] In the left menu, select **Team**. The list shows everyone with access to your clubs: their **Clubs**, their **Role** and the **Planning** categories they can see. <figure> <img src="/images/guide/admins/team.png" alt="Team section listing staff members with their email, clubs, role (Owner, Admin or Staff) and planning access, with Invite staff and Invite admin buttons." width="1280" height="800" /> <figcaption> The example team of Ukab Clubs and its sister clubs. </figcaption> </figure> There are three [roles](/en/guide#role): | Role | What they can do | | --------- | -------------------------------------------------------------------- | | **Owner** | The account that owns your clubs. Full access. | | **Admin** | Everything, in the clubs they are assigned to. | | **Staff** | Only the tasks you tick for them, in the clubs they are assigned to. | ## 2. Invite a staff member [#2-invite-a-staff-member] Select **Invite staff**. 1. Enter their **Email**. 2. In **Club access**, tick each club they work for. 3. For each club, tick what they may do. Permissions are grouped by topic: **Members and applications**, **Daily operations**, **Finance** and **Club configuration**. 4. In **Accessible planning categories**, tick the activities whose planning they should see, such as private lessons for a coach. 5. Select **Send invitation**. <figure> <img src="/images/guide/admins/invite-staff.png" alt="Invite staff dialog with an example email, Ukab Clubs ticked in Club access and three permissions selected, including View member profiles and View assigned planning." width="1280" height="800" /> <figcaption> Inviting an example coach who can view member profiles, see their planning and set their own availability. </figcaption> </figure> > Give only the permissions the person needs today. You can add more at any time by opening their > line in **Team**, and changes apply as soon as you save. [Who sees > what](/en/guide/admins/who-sees-what) lists what each permission shows. To give someone full access, select **Invite admin** instead and tick their clubs. ## 3. What your colleague receives [#3-what-your-colleague-receives] Your colleague receives an email with an invitation link. The link opens a **Join the back office** page where they enter their name, choose a password and select **Accept invitation**. They can then [sign in](/en/guide/staff/getting-started). <figure> <img src="/images/guide/admins/join-back-office.png" alt="Join the back office page with Your name and Password fields, the line Already have an account? Use your existing password., an Accept invitation button outlined in orange and a Back to sign in link." width="448" height="388" /> <figcaption> The page an invitation link opens. </figcaption> </figure> Until they accept, their line in **Team** shows **Invitation pending**. To withdraw an invitation, select the envelope icon with a cross next to that label: its tooltip reads **Cancel invitation**. An invitation that was never accepted eventually shows **Invitation expired**: send a new one. To change a colleague's access later, add their professional documents or remove them from your team, see [Manage your team](/en/guide/admins/manage-your-team). # Manage your team Find a colleague, change what they can do, follow their activity and remove their access. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/manage-your-team Once a colleague has joined, everything about them is kept in their staff file in the **Team** section: the profile members see, their qualifications, their access, their hours, their reservations and a log of what they did in the [back office](/en/guide#back-office). To add someone to your team, see [Invite your team](/en/guide/admins/invite-your-team). ## 1. Find a colleague [#1-find-a-colleague] In the left menu, select **Team**. Type part of a name or an email address in **Search by name or email…** to shorten the list. To narrow the list further, select **Filters**, choose your filters and select **Apply filters**. **Reset** clears them. <figure> <img src="/images/guide/admins/team-filters.png" alt="Team section with the Filters window open over the list: Role set to All roles, Status set to All statuses, Professional documents set to Expired documents, and Reset, Cancel and Apply filters buttons." width="1280" height="800" /> <figcaption> Looking for expired documents in the example team of Ukab Clubs. The control to use is outlined in orange. </figcaption> </figure> | Filter | Choices | | -------------------------- | ------------------------------------------------------------------------------- | | **Role** | **All roles**, **Admins** or **Staff**. | | **Status** | **All statuses**, **Joined**, **Invitation pending** or **Invitation expired**. | | **Professional documents** | **All documents**, **Expired documents** or **Expires within 30 days**. | When a colleague has a document that has expired or expires within 30 days, their line shows it, for example **1 expired** or **Expires in 12 d**. Expired documents show first. <figure> <img src="/images/guide/admins/team-expired-documents.png" alt="Team list filtered to one line, Sport coach with ukab.staff@example.com, a red 1 expired label, the Ukab Clubs club, Staff role and seven planning categories, and the Filters (1) button at the top right." width="1280" height="800" /> <figcaption> With the Expired documents filter, only the example coach Sport coach remains. </figcaption> </figure> Select a colleague's line to open their file. It has five tabs: **Profile**, **Access**, **Availability**, **Reservations** and **History**. To edit their profile and professional documents, see [Your team's profiles and documents](/en/guide/admins/team-documents). ## 2. Change what they can do [#2-change-what-they-can-do] Open the **Access** tab. It shows the same choices as the invitation: **Club access**, then, for each club, the permissions grouped by topic and the **Accessible planning categories**. <figure> <img src="/images/guide/admins/team-access.png" alt="Access tab of Sport coach: Ukab Clubs ticked in Club access, the Ukab Clubs section summarized as 7 permissions selected and 7 assigned categories, View member profiles marked Included automatically, and View assigned planning, Create and change reservations, Validate attendance and tickets and Manage their own availability ticked, with Remove from space, Cancel and Save changes buttons." width="1280" height="800" /> <figcaption> The access of the example coach Sport coach at Ukab Clubs. </figcaption> </figure> * Each club's title sums up its access, for example **7 permissions selected · 7 assigned categories**. **Modified** appears next to a club you changed. * **Included automatically** marks a permission that comes with another one you ticked. For example, **Edit staff notes** includes **View member profiles**. * If the person can see the planning without any category, the club shows **Planning has no assigned category**: tick at least one category. Select **Save changes**. Changes apply immediately. The **Access** tab is not available for the owner of your clubs, who always has full access. ## 3. See their hours, reservations and history [#3-see-their-hours-reservations-and-history] * **Availability** shows their working hours week by week, as described in [Your reservations and availability](/en/guide/staff/schedule-and-availability). Each colleague changes their own hours from **My availability**. * **Reservations** lists the reservations made with them, for example their lessons. * **History** shows the **Activity history**: what this colleague did in the back office, such as **Created a reservation**, **Sent a newsletter** or **Changed a team member's access**, with the person concerned, the date and the club. When a line has an arrow, select it to see the details of the change. A colleague who has done nothing yet shows **No recorded actions.** <figure> <img src="/images/guide/admins/team-history.png" alt="History tab of Sport coach with two lines: Reported product consumption for Jamie Chen yesterday at Ukab Clubs, opened to show Products changed to Sparkling water × 2, Champagne bottle × 1, and Checked out a reservation for Sam Rivera three days ago." width="1280" height="800" /> <figcaption> The history of the example coach, with the minibar products reported for Jamie Chen's stay. </figcaption> </figure> When a colleague works for several clubs, choose the **Club** at the top of the **Availability** and **Reservations** tabs. ## 4. Remove a colleague [#4-remove-a-colleague] 1. Open their file, then the **Access** tab. 2. Select **Remove from space**. 3. Read the message, then select **Remove access** to confirm, or **Keep access** to go back. They lose access to every club of your space immediately. To remove them from one club only, untick that club in **Club access** and select **Save changes**. # Application forms Build the forms people fill in to join your club, and choose what they complete once accepted. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/onboarding-forms People join your club by filling in an [application](/en/guide#application) form. In Champagne, each form is called an onboarding. You can have several, for example one for regular members and one for founding members, each with its own questions and [plans](/en/guide#plan). You set them up from the **Applications** section. To read and decide on the applications you receive, see [Review applications](/en/guide/staff/applications). ## 1. Open your forms [#1-open-your-forms] In the left menu, select **Applications**, then **Manage onboardings**. Each card shows a form's name and description, with its **Onboarding ID**, the code that identifies it. Select **Copy ID** to copy it, for example for the developer of your website. <figure> <img src="/images/guide/admins/onboarding-list.png" alt="Onboardings page with two cards, Common members and Founding members, each with its description, Onboarding ID, a Copy ID button and an Edit onboarding button, the first one outlined in orange, and an Add onboarding button at the top right." width="1280" height="800" /> <figcaption> The two example forms of Ukab Clubs. </figcaption> </figure> * To change a form, select **Edit onboarding** on its card. * To create one, select **Add onboarding**, type a **Name** your staff will recognize and a **Description** of who should use it, then select **Create onboarding**. The new form opens. **Back to onboardings**, at the top of a form, takes you back to the list. If a form shows **Onboarding or club unavailable**, it may have been removed or may belong to another club: check the club selected at the top left. ## 2. Name the form and its hosted page [#2-name-the-form-and-its-hosted-page] The top of the form holds two cards. * **Onboarding settings**: the **Name** and **Description** applicants see when they choose how to join. * **Hosted application settings**: Champagne can publish the form on a ready-made, branded page. Turn on the switch, then choose a **Font** (**Modern**, **Classic** or **Editorial**) and a **Primary color**. The card gives the address of the page, which goes live once you save. <figure> <img src="/images/guide/admins/onboarding-settings.png" alt="Edit onboarding page for Common members with a Save onboarding button, the Onboarding settings card with its name, description and ID, and the Hosted application settings card switched on with the Modern font and a blue primary color." width="1280" height="800" /> <figcaption> The example form Common members of Ukab Clubs, published on a hosted page. </figcaption> </figure> To set up the rest of your club's website, see [Your club's website](/en/guide/admins/hosted-site). ## 3. Build the questions [#3-build-the-questions] Below the settings, the form is a list of blocks, shown to applicants in this order. Use the arrows on a block to move it up or down, and the bin to remove it. To add a block, use the buttons right below the blocks, above the **After approval** card. A form holds up to 50 blocks. | Block | What the applicant does | | ------------------------- | ----------------------------------------------------------------------------------------------------------- | | **Applicant name** | Types their first and last name. Every form has it. | | **Email verification** | Types their email address, then a code sent to it. Every form has it. | | **Question** | Answers a question you write. | | **Applicant address** | Types their postal address. It is copied to their profile. | | **Plan selection** | Picks one of the membership plans you tick in the block. | | **Phone verification** | Types their mobile number, then a code texted to it. It is copied to their profile. Always required. | | **Identity verification** | Verifies an ID document with Stripe Identity, as described in [step 4](#4-choose-the-steps-after-approval). | | **Visit booking** | Books a free visit, as described in [step 4](#4-choose-the-steps-after-approval). | | **Sponsorship** | Types the sponsor codes club members gave them. You set the **Number of sponsors**. | **Applicant address**, **Plan selection**, **Phone verification**, **Identity verification**, **Visit booking** and **Sponsorship** can each be added once. **Identity verification** only appears once Stripe Identity is set up for Champagne. Place **Identity verification** and **Visit booking** below **Applicant name** and **Email verification**: the arrows cannot move them above. **Applicant address**, **Phone verification**, **Identity verification** and **Visit booking** can be asked either here, before approval, or in the **After approval** card, but not both: a block already added here is greyed out there with **Already asked before approval.**, and the other way round. A visit booked before approval is moved to the new member's profile when you accept the application, and canceled when you reject it. In a **Plan selection** block, tick the plans applicants can choose under **Plans included in onboarding**. Other plans remain available to members once they have joined. If your club has no plan yet, the block shows **No plans are available for this club. Add a plan first.**: see [Plans and pricing](/en/guide/admins/plans-and-pricing). For each **Question**: 1. Type its **Title**, for example "Why would you like to join?". 2. Choose its **Type**: **Short answer** for a line of text, **Long answer** for a paragraph, or **Options** for a choice between 2 and 20 answers you type. 3. In **Saves to**, choose where the answer goes. **Application only** keeps it with the application. A field from **Person fields**, such as **Phone** or **Date of birth**, or one of your **Member attributes** copies the answer to the new member's profile. Each field can receive one question only. 4. Tick **Required** if the applicant must answer. <figure> <img src="/images/guide/admins/onboarding-questions.png" alt="Three question blocks: Why would you like to join? as a required long answer, Your phone number as a short answer saved to Phone, and Which language do you prefer? as options saved to Preferred language, with the Saves to list open on Application only, Person fields and Member attributes." width="1280" height="800" /> <figcaption> The questions of the example form Common members, with the Saves to list open. The control to use is outlined in orange. </figcaption> </figure> When a question saves to a field with fixed answers, such as **Gender**, the applicant chooses among that field's answers, listed under the question. ## 4. Choose the steps after approval [#4-choose-the-steps-after-approval] The **After approval** card lists what accepted applicants still complete, from the link in their approval email. * **Collect plan payment**: the applicant pays the plan they picked. This needs a **Plan selection** block in the form: without one, the option is greyed out and marked **Plan selection required**. * **Require a document signature**: the applicant signs a PDF online, such as your club rules. Select **Upload PDF** to add the document, up to 2 MB. If you try to save without a document, the option shows **Document required.** If the file is refused, a **Document upload failed** message gives the reason, for example a file that is not a valid PDF or is too large. Choose another file and try again. * **Collect the applicant's address**: the applicant types their postal address, which is saved to their profile at once. * **Verify the applicant's identity**: the applicant photographs an ID card, passport or driving licence with Stripe Identity, which checks that the document is genuine and not expired. Set the checks below the option: * **Minimum age**: compared with the date of birth on the document, 18 by default. Enter 0 to skip the age check. * **Require a selfie**: Stripe compares the applicant's face with the document photo. * **Require the name to match**: the name on the document must contain the first and last name given in the application. Middle names on the document are accepted. The option only appears once Stripe Identity is set up for Champagne. If a form already has this step and Stripe Identity is no longer available, the option says so: untick it to save the form. Champagne keeps only the result of each check and the date of birth, never the photos or the document number. * **Verify the applicant's phone**: the applicant confirms a mobile number with a texted code, which is saved to their profile at once. * **Book a visit**: the applicant picks a time in a reservation category, for example a discovery visit, and can change or cancel it until they continue. Choose the **Reservation category**: only categories booked by time slot are listed, and categories with prices cannot be chosen, because onboarding never takes payment. Turn off **Required** to let applicants skip the step. Turn on **Book only through onboarding** in the [category options](/en/guide/admins/reservation-categories) to hide the category from members. The booking follows the category's opening hours, notice and capacity, appears in the planning, and the application shows the booked visit with a link to the reservation. Accepted applicants complete these steps in this order: payment, signature, address, identity, phone, then visit. * **Applicant continuation URL**: leave it empty to keep these steps on Champagne. Fill it in only if your own website takes over after approval. <figure> <img src="/images/guide/admins/onboarding-after-approval.png" alt="The buttons to add a block, from Question to Email verification with 7 / 50 blocks, then the After approval card with Collect plan payment and Require a document signature ticked, the club-rules.pdf document, an empty Applicant continuation URL field, and the Save onboarding button outlined in orange." width="1280" height="800" /> <figcaption> The example form asks accepted applicants to pay their plan and sign the club rules. </figcaption> </figure> ## 5. Save the form [#5-save-the-form] Select **Save onboarding**. Until then, the page shows **Unsaved changes** next to the form's name, and Champagne asks before you leave the page. Saving takes you back to your forms. > If a colleague saves the same form while you edit it, a **This onboarding changed** window opens. > Select **Reload** to load their version, or **Keep editing** to go on with yours: saving then > replaces their version. ## What applicants see [#what-applicants-see] Applicants pick a form on your club's page and fill it in one step at a time. See [Find a club and apply](/en/guide/members/find-a-club-and-apply) and [Complete and sign your application](/en/guide/members/sign-your-application). The plans they can choose come from [Plans and pricing](/en/guide/admins/plans-and-pricing). # Payments Connect your club to Stripe so members can pay online, and set up your club's credits. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/payments Members pay your club online by card through Stripe, an online payment service. The money goes to your club's Stripe account, called a merchant in Champagne. Once your club is connected to a merchant, members can pay their membership, top up their [credits](/en/guide#credits) and pay their invoices from their account. You set this up in **Settings**, on the **Payments** tab. ## 1. Check your club's merchant [#1-check-your-clubs-merchant] In the left menu, select **Settings**, then the **Payments** tab. The **Stripe merchant** card shows **Configured** when your club is connected, and **Not configured** otherwise. <figure> <img src="/images/guide/admins/payments-merchant.png" alt="Payments tab with the Stripe merchant card marked Configured: the Ukab Clubs merchant selected for this club, its merchant information with country France and entity type Individual, a Payment readiness list where Payments, Payouts and Catalogue are Enabled or Ready, and a Verify catalogue button." width="1280" height="800" /> <figcaption> The example merchant of Ukab Clubs, ready to take payments. Its email is blurred. </figcaption> </figure> **Payment readiness** tells you what works today: | Line | Ready when it shows | Meaning | | ------------- | ------------------- | ------------------------------------------------------------- | | **Payments** | **Enabled** | Stripe accepts card payments for your club. | | **Payouts** | **Enabled** | Stripe can transfer the money collected to your bank account. | | **Catalogue** | **Ready** | Your plans are prepared on Stripe, so members can buy them. | When a line is not ready, it shows **Not enabled** for **Payments** and **Payouts**, or **To prepare** for **Catalogue**. Section 2 explains what to do. The status updates on its own while the page is open. **Last checked** shows when it was last refreshed. Other messages can appear on the card: | Message | Meaning | | --------------------------------- | ----------------------------------------------------------------------------------------------------------- | | **Checking for updates…** | Champagne is asking Stripe for the latest status. | | **Automatic updates interrupted** | Champagne could not get the latest status. The card shows the last known one. Reload the page to try again. | | **Not checked yet** | Shown after **Last checked** when Champagne has not received a status from Stripe yet. | | **Updating merchant…** | A change you asked for, such as **Prepare catalogue**, is in progress. Wait for it to finish. | ## 2. Connect a club to Stripe [#2-connect-a-club-to-stripe] If the card shows **Not configured**, you have two options. * Use an existing merchant. Merchants you already created for your other clubs are listed with their details. Select **Use this merchant** on the right one: both clubs then collect payments on the same Stripe account. * Create a new merchant. Select **Create a merchant** and fill in the form. <figure> <img src="/images/guide/admins/payments-choose-merchant.png" alt="Stripe merchant card marked Not configured for the Vosges club: the existing Ukab Clubs merchant with a Use this merchant button, then a new merchant form filled with the name Vosges and the email billing@example.com, the country FR, an empty legal form, the entity type Company, and Create merchant and Cancel buttons." width="1044" height="782" /> <figcaption> Connecting the example club Vosges: reuse the Ukab Clubs merchant or create a new one. The control to use is outlined in orange. </figcaption> </figure> To create a merchant: 1. Enter the **Merchant name**, usually your club or company name, and a contact **Email**. 2. Enter the **Country (ISO code)**, the two-letter code of your country, such as **FR** for France. 3. Optionally enter the **Legal form**, such as SAS, SARL or association. 4. Choose the **Entity type**: **Company**, **Individual**, **Non Profit** or **Government Entity**. 5. Select **Create merchant**. The new merchant then shows **Configuration to complete**. Select **Complete Stripe activation**: Stripe opens in a new tab and asks for your company details and bank account. Once you finish there, come back to Champagne. The card updates by itself. > If Stripe does not open, allow pop-ups for Champagne in your browser and select **Complete Stripe > activation** again. When **Catalogue** shows **To prepare**, select **Prepare catalogue** to publish your [plans](/en/guide#plan) on Stripe. Later, **Verify catalogue** checks that they are still in place. ## 3. Name your club's credits [#3-name-your-clubs-credits] Credits are a currency of your own that members buy in packs and spend on bookings. The **Club credits** card on the same tab sets them up. <figure> <img src="/images/guide/admins/payments-credits.png" alt="Club credits card with the credit name Stars, 10 credits per €1, a low balance alert of €10.00, a Manage credit packs link and a Save button." width="1044" height="308" /> <figcaption> The example credits of Ukab Clubs, called Stars. </figcaption> </figure> 1. Enter the **Credit name** members will see, for example "Stars". 2. In **Credits per €1**, set the standard value of a credit. It is used to show how much members save with each pack. Changing it does not change existing balances or packs. 3. In **Low balance alert**, enter the amount under which members are emailed. Enter 0 to turn the alert off. 4. Select **Save**. To create the packs members buy, select **Manage credit packs**. See [Plans and pricing](/en/guide/admins/plans-and-pricing). ## 4. What members pay online [#4-what-members-pay-online] Once your merchant is ready, members pay by card on a secure Stripe page: | What | Where members pay it | | ------------------- | ---------------------------------------------------------------------------------------------------------------- | | Membership | When they choose a plan, and then at each renewal. See [Your membership](/en/guide/members/membership-and-credits). | | Credit packs | With **Top up for …** in their credits. | | Invoices | With **Pay on Stripe** in their invoices. See [Billing](/en/guide/members/billing). | | Non-member bookings | By card, for categories open to non-members. | Members pay their bookings with credits when a resource has a price in credits. Prices and deposits of each resource are set in its own settings: see [Resources, prices and products](/en/guide/admins/resources-and-prices#2-set-a-resources-rules). To follow what was paid, see [Sales and invoices](/en/guide/staff/sales-and-invoices). # Plans and pricing Create membership plans, credit packs and add-ons, and change a plan's price. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/plans-and-pricing The **Plans** section lists everything your club sells online: memberships, credit packs and add-ons. Members pay for memberships and credit packs by card through Stripe, the online payment service connected to your club, and for add-ons with their [credits](/en/guide#credits). > If your club has no Stripe account yet, the page shows **Connect a Stripe merchant before adding > plans** and **Add plan** stays greyed out. Select **Open payment settings**, then follow > [Payments](/en/guide/admins/payments). ## 1. See your plans [#1-see-your-plans] In the left menu, select **Plans**. Each line shows the [plan](/en/guide#plan)'s name and description (the credits included, for a credit pack), its **Type**, its **Amount** and its **Interval**. Select a column title to sort the list. A club without plans shows **No plans yet.** If the list shows **Plans unavailable**, your plans could not be loaded: reload the page. <figure> <img src="/images/guide/admins/plans.png" alt="Plans section with an Add plan button and six plans: three yearly subscriptions priced €1,800, €4,500 and €6,300, and three one-time credit packs, Discovery with 250 Stars for €25, Regular with 1,250 Stars for €100 and Weekend with 600 Stars for €50." width="1280" height="800" /> <figcaption> The example plans of Ukab Clubs: three yearly memberships and three credit packs. </figcaption> </figure> There are three types of plan: | Type | What the member buys | Price | | ---------------- | -------------------------------------------------------- | -------------------- | | **Subscription** | A membership that renews every month or every year. | An amount in money. | | **Credit pack** | A one-time top-up of club credits, used for bookings. | An amount in euros. | | **Add-on** | A one-time extra the member pays for with their credits. | A number of credits. | Credits are your club's own currency. You choose their name, such as Stars, and their value under **Club credits** in [Payments](/en/guide/admins/payments). ## 2. Add a plan [#2-add-a-plan] Select **Add plan**. 1. Type a **Name**. 2. Choose the **Type**. The price fields of that type appear below. 3. If you like, type a **Description**. Members see both the name and the description. 4. Fill in the price: * **Subscription**: the **Amount**, its **Currency** and the **Billing interval**, **Monthly** or **Yearly**. * **Credit pack**: the **Amount (EUR)** and the number of **Credits included**. * **Add-on**: the price in credits, for example **Price in Stars ⭐**. 5. Select **Create plan**. <figure> <img src="/images/guide/admins/add-plan.png" alt="Add plan window with the Name Family, the Type set to Credit pack, the Description Shared top-up for families, 200 in Amount (EUR) and 2600 in Credits included, and Cancel and Create plan buttons." width="1280" height="800" /> <figcaption> Preparing an example credit pack, Family, with 2,600 credits for €200. The control to use is outlined in orange. </figcaption> </figure> The type, currency and billing interval are fixed once the plan exists. To change them, create a new plan. ## 3. Edit a plan [#3-edit-a-plan] Select the pencil on a plan's line, change it, then select **Save**. * For a **Credit pack** or an **Add-on**, you can change the name, the description and the price. * For a **Subscription**, you can change the name and the description. The window reminds you: **To change the price, split the plan.** ## 4. Change the price of a membership [#4-change-the-price-of-a-membership] Changing a membership's price is called a split. Select **…** on the plan's line, then **Split**. Only a subscription that was never split offers it. * **Current subscribers**: the members on this plan stay on it. Set the **Price** they will pay. If you change it, they are notified at once and pay the new price from their next payment, or in one month if that payment is sooner. * **New subscribers**: type a **New plan name**, a **Description** and the **Price**. This new plan replaces the old one in your [application](/en/guide#application) forms and at checkout from today. Select **Split plan** to confirm. <figure> <img src="/images/guide/admins/split-plan.png" alt="Split window for the Adhésion 51 IÉNA (C) - Abonnement Cercle plan with two columns: Current subscribers, with no current subscribers, keeping the plan at 4500 euros, and New subscribers with a new plan named Adhésion 51 IÉNA (C) - Abonnement Cercle 2027 priced 4800 euros, and a Split plan button outlined in orange." width="1280" height="800" /> <figcaption> Raising the example Abonnement Cercle membership to €4,800 for new members while current members keep €4,500. </figcaption> </figure> After a split, the old plan's line shows **Current subscribers only. Replaced by …** with the name of the new plan. > To raise the price for newcomers only, leave the **Price** under **Current subscribers** > unchanged. ## 5. Delete a plan [#5-delete-a-plan] Select **…** on the plan's line, then **Delete**, and confirm with **Delete plan**. Members can no longer buy it. Existing memberships keep the details of the plan they paid for. ## What members see [#what-members-see] Members choose a membership and buy credit packs and add-ons from **My account**. See [Your membership](/en/guide/members/membership-and-credits) and [Your credits](/en/guide/members/credits). To offer a membership while people apply to join, add a **Plan selection** block to your [application forms](/en/guide/admins/onboarding-forms). # Run the platform Create spaces and clubs for new customers, choose their owner, turn modules off and suspend a club. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/platform-administration This page is for the people who run Champagne itself, not for club staff. A platform admin creates a space for each customer, adds its clubs, chooses its owner and decides which modules each club uses. The role gives no access to the clubs' members, bookings or invoices. ## 1. Open the Platform section [#1-open-the-platform-section] In the left menu, select **Platform**. Each card is a space: its owner, then its clubs with their timezone and hosted address. <figure> <img src="/images/guide/admins/platform.png" alt="Platform section showing the Champagne space owned by Demo Administrator, with its clubs Astel, Ukab Clubs and Vosges, each with a Modules button and an Active switch." width="1280" height="800" /> <figcaption> The demo space and its three clubs. </figcaption> </figure> ## 2. Create a space for a new customer [#2-create-a-space-for-a-new-customer] Select **New space**. 1. Enter the **Space name**, usually the customer's company. 2. Enter the **Owner email**. 3. Describe the first club: **Club name**, **Timezone** and, if it takes applications online, its **Hosted address**. 4. Select **Create space**. <figure> <img src="/images/guide/admins/platform-new-space.png" alt="New space form filled in for Riviera group, with the owner email, the club name Riviera Nice, the timezone Europe/Paris and the hosted address riviera-nice." width="1280" height="800" /> <figcaption> A new space with its first club. </figcaption> </figure> If the email already has a Champagne account, that person becomes owner at once. Anyone else receives an invitation by email, valid for 7 days. Until they accept, the card shows the pending invitation and the date it expires; select **Resend invitation** to send a new one, or **Cancel invitation** to withdraw it. The owner is admin of every club in the space and invites the rest of the team from the **Team** section, as described in [Invite your team](/en/guide/admins/invite-your-team). ## 3. Add a club, edit or change the owner [#3-add-a-club-edit-or-change-the-owner] * **Add club** adds another club to the space. Its owner becomes admin of it. * **Rename** on a space changes its name. **Edit** on a club's row changes its name, timezone and hosted address. * Each space needs its own name, and so does each club within a space. A club name always starts with an uppercase letter. * **Change owner** hands the space over to another email. The current owner stays owner until the new one accepts. **Remove the access of …** is checked by default: the previous owner then loses access to every club of the space when the new owner takes over. Uncheck it to keep them as admin, for example a partner who stays in the business. ## 4. Turn modules off for a club [#4-turn-modules-off-for-a-club] Select **Modules** on a club's row. Every module is on for a new club. Turn off the ones the customer does not use, then select **Save**. <figure> <img src="/images/guide/admins/platform-modules.png" alt="Modules of Ukab Clubs, with a switch for Reservations, Staff availability, Customer chats, Housekeeping, Newsletters, Applications and Finance." width="1280" height="800" /> <figcaption> The modules of Ukab Clubs. </figcaption> </figure> A module turned off disappears from the back office of that club for everyone, club admins included, and its actions are refused. The button then counts the modules turned off, for example **Modules (2 off)**. **Finance** is the only module that also changes the member area. Turn it off for a club that charges nothing: * Members no longer see credits, top-ups, plan choice, payment or prices. Paid onboarding steps are skipped, including in applications already in progress. * Staff no longer see prices, deposits, paid products or credit settings, and cannot add plan steps to an onboarding. * Champagne refuses every new charge for the club: checkout, top-ups, credit spending, deposits and refunds. Existing invoices, credit balances and paid memberships are kept as they are. Turning **Finance** back on shows them again. ## 5. Suspend a club [#5-suspend-a-club] Turn off the **Active** switch of a club, then confirm with **Suspend club**. <figure> <img src="/images/guide/admins/platform-suspend.png" alt="Confirmation asking whether to suspend Vosges, explaining that its staff and members can no longer sign in, with a Suspend club button." width="1280" height="800" /> <figcaption> Suspending asks for confirmation. </figcaption> </figure> A suspended club keeps all its data, but its staff and members can no longer sign in to it and its public pages close: hosted site, online booking, membership applications and offers. Its staff see that the club is suspended instead of the club, and members see it on the sign-in page. Turn the switch back on to reactivate it: everything works again at once. ## 6. Archive a club or a space [#6-archive-a-club-or-a-space] When a customer leaves, or never finished setting up, select **Archive** on the club's row or on the space, then confirm. An archived club is closed like a suspended one and leaves the list; archiving a space archives all its clubs. Nothing is deleted. Turn on **Show archived** at the top of the page to see them again, then select **Restore**. Restoring a space puts each club back as it was before: an active club reopens, a suspended club stays suspended and a club archived on its own stays archived. A club of an archived space can only come back with its space. ## 7. Manage the platform admins [#7-manage-the-platform-admins] Open the **Platform admins** tab at the top of the Platform section. It lists everyone with the role. * To add someone, enter their **Email** and select **Add platform admin**. If they already have a Champagne account, they get the role at once. Otherwise also enter their **Name**: they receive an email to choose their password, valid for 7 days. * To take the role away, select **Remove role** on their row and confirm. Their account and any club access stay. You cannot remove yourself, so there is always at least one admin. Tools such as Terraform create and manage clubs with a platform token instead of an account. Open the **API tokens** tab, select **Create a token**, enter its **Name**, choose its **Access** and, if you want one, an **Expiration date (optional)**, and select **Create**. Copy the token at once: it is shown a single time. A platform token manages clubs and reads no club data. To cut a tool off for a while, open the **⋯** menu on the row of its token and select **Block**; **Unblock** gives the access back. To cut it off for good, select **Delete** and confirm. ## 8. Check what changed [#8-check-what-changed] The **History** tab lists every change made from the Platform section, newest first: who made it, when, on which space, club or admin, and the values before and after. Club staff never see these entries. # Set up reservation categories Create what members can book and decide when, for how long and how far ahead they can book it. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/reservation-categories Everything members can book is organized in [reservation categories](/en/guide#reservation-category), such as a restaurant, hotel suites, tennis lessons or a yoga class. Each thing that can be booked in a category, such as a table, a suite or a coach, is a resource. This page shows how to create a category and set its booking rules. To price resources and sell products, see [Resources, prices and products](/en/guide/admins/resources-and-prices). To schedule group sessions, see [Sessions, guest lists and blocked periods](/en/guide/admins/sessions-and-blocked-periods). You work from the **Reservations** section. The buttons used here only appear if your [role](/en/guide#role) lets you change categories. ## 1. Create a category [#1-create-a-category] In the left menu, select **Reservations**, then the **+** button at the end of the category strip. When your club has no category yet, the page shows **No reservation categories yet.** <figure> <img src="/images/guide/admins/category-add.png" alt="Add reservation category window on the Setup step, with Restaurant selected under Start from, the name La Brasserie, and Cancel and Next buttons. The Resources and Options steps are listed on the left." width="1280" height="800" /> <figcaption> Starting an example restaurant category, La Brasserie. The control to use is outlined in orange. </figcaption> </figure> 1. **Setup**: choose what to **Start from**. Each starting point fills in sensible settings that you can change later. | Start from | What it prepares | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Custom** | Nothing: you choose every setting. | | **Restaurant** | Booking by party size, 2-hour bookings starting every 30 minutes, bookable up to 60 days ahead with 60 minutes' notice, a waitlist, and lunch and dinner opening hours. | | **Group class** | Booking by session, with a QR code ticket. | | **Treatment** | 1-hour bookings with 2 hours' notice, customer sheets and opening hours from 09:00 to 19:00, Monday to Saturday. | | **Room or suite** | Housekeeping checklists for each room. | Then type a **Name** customers will recognize, and select **Next**. 2. **Resources**: type the name of each resource, such as "Table 1" or "Garden suite". Select **Add resource** for each new one. If you leave every resource blank, the category gets one resource with its own name. Select **Next**. 3. **Options**: choose **How members book** and the optional tools described in the next sections, then select **Create category**. Use **Back** to return to a previous step. Once the category exists, select it in the strip and select **Category settings** to change anything. The window has five tabs: **General**, **Resources**, **Booking**, **Hours** and **On site**. Select **Save** to apply your changes. ## 2. Choose how members book [#2-choose-how-members-book] On the **Booking** tab, **How members book** sets the booking flow of the category: | Choice | What members do | | --------------------------- | -------------------------------------------------------------------------------------------------- | | **A resource and a time** | Pick a resource, such as a suite, and the dates or times. | | **A party size and a time** | Give the number of people and a time. The smallest free resource that fits is assigned. | | **A session** | Join one of the sessions you schedule. See [Sessions](/en/guide/admins/sessions-and-blocked-periods). | | **A request to discuss** | Send a request, then chat with your team before a date is set, for example for a private lesson. | Two switches complete the choice: * **Waitlist**: members can wait for a full session or time to free up. Only with sessions or party-size booking. * **Non-members**: people without a membership can book the resources you open to them and pay by card. Only when members choose a resource and a time. See [Prices](/en/guide/admins/resources-and-prices#3-set-its-prices). * **Book only through onboarding**: members no longer see the category; applicants book it in the booking step of an onboarding, for example a discovery visit. Staff still see its reservations in the planning. Cannot be combined with **Non-members**. See [Onboarding forms](/en/guide/admins/onboarding-forms#4-choose-the-steps-after-approval). ## 3. Set the booking length and booking window [#3-set-the-booking-length-and-booking-window] The rest of the **Booking** tab sets when and for how long people can book. <figure> <img src="/images/guide/admins/category-booking.png" alt="Booking tab of Le Restaurant's settings: Booking length of 120 minutes, Start times Every 30 minutes, Bookable up to 60 days ahead, Minimum notice of 60 minutes, and the Linked categories section with the Cours collectif checkbox." width="1280" height="800" /> <figcaption> The booking rules of the example restaurant Le Restaurant. </figcaption> </figure> | Setting | What it does | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | **Booking length (minutes)** | The fixed length of a booking. Members cannot change it. Leave it empty to let them choose the end. Required for party-size booking. | | **Start times** | For party-size booking: how often a booking can start, from **Every 5 minutes** to **Every 60 minutes**. | | **Bookable up to (days ahead)** | How far ahead members can book. Leave empty for no limit. | | **Minimum notice (minutes)** | How long before the start a booking must be made. Leave empty for no limit. | | **Linked categories** | Other categories a reservation can move to. Staff can move reservations there, and members are offered them when a time is full. | ## 4. Set the opening hours [#4-set-the-opening-hours] On the **Hours** tab, set the **Opening hours** of each day. Members and non-members can only book inside these hours. Your team can book at any time. <figure> <img src="/images/guide/admins/category-hours.png" alt="Hours tab of Le Restaurant's settings: Monday marked Closed with an Add hours button, Tuesday and Wednesday open from 12:00 to 14:30 and from 19:00 to 23:00, each range with a bin button, and Monday's Add hours button outlined in orange." width="1280" height="800" /> <figcaption> The example restaurant is closed on Mondays and open for lunch and dinner. </figcaption> </figure> * Select **Add hours** under a day to add an opening and a closing time. A day can have several ranges, such as lunch and dinner. * Select the bin next to a range to remove it. A day without hours shows **Closed**. * When no day has hours, the category shows **Open at any time.** ## 5. Turn on the on-site tools [#5-turn-on-the-on-site-tools] Most tools are on the **On site** tab of **Category settings**. <figure> <img src="/images/guide/admins/category-tickets.png" alt="On site tab of the Événement category settings: QR code ticket turned on with the description Scan at check-in and outlined in orange, Housekeeping and Customer sheets turned off, and Delete category, Cancel and Save buttons." width="1280" height="800" /> <figcaption> The example Événement category gives each booking a QR code ticket. </figcaption> </figure> | Where | Switch | What it adds | | ----------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | | **Resources** tab | **Floor plan** | A plan of the space under the calendar. See [Floor plan editor](/en/guide/admins/floor-plan-editor). | | **On site** tab | **QR code ticket** | A ticket members show at check-in. See [Check guests in and out](/en/guide/staff/check-in-and-deposits). | | **On site** tab | **Housekeeping** | A checklist and a cleaning status for each resource. See [Housekeeping](/en/guide/staff/housekeeping). | | **On site** tab | **Customer sheets** | A sheet for each customer's preferences and contraindications, for example for treatments. | ## 6. Delete a category [#6-delete-a-category] Open **Category settings** and select **Delete category**. The window asks you to confirm: the category, its sessions and its blocked periods are deleted, and this cannot be undone. Select **Delete category** again to confirm. # Resources, prices and products List what can be booked in a category, set each resource's rules and prices, and sell products with it. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/resources-and-prices Each thing that can be booked in a [reservation category](/en/guide#reservation-category), such as a table, a suite or a coach, is a resource. This page shows how to add resources to a category, set their rules and prices, and sell products with them, such as a minibar in a suite. To create the category itself, see [Set up reservation categories](/en/guide/admins/reservation-categories). ## 1. List the resources of a category [#1-list-the-resources-of-a-category] In the left menu, select **Reservations**, select the category in the strip, then **Category settings**. Open the **Resources** tab. * Rename a resource in its field. * Add one with **Add resource**. * Remove one with its bin. A resource that already has reservations shows a padlock: you can rename it, but not delete it. In a category with a floor plan, resources are added and removed on the plan instead: see [Floor plan editor](/en/guide/admins/floor-plan-editor). Select **Save** to apply your changes. ## 2. Set a resource's rules [#2-set-a-resources-rules] Select the settings button next to a resource to open its own settings. The **Resource** tab holds its rules and prices. <figure> <img src="/images/guide/admins/resource-settings.png" alt="Settings of the Courtyard suite: Security deposit in Stars of 500, Maximum occupants 4, empty check-in and check-out fields, a Cancellation notice of 2 days with its explanation, the Bookable externally switch on, and a Price section with a Default price of 250 Stars charged Per day, a Non-members price of 320 euros per day and an Add plan price button outlined in orange." width="1280" height="800" /> <figcaption> The example Courtyard suite costs 250 Stars a day, or €320 for non-members, with a 500-Star deposit and 2 days' cancellation notice. The control to use is outlined in orange. </figcaption> </figure> | Setting | What it does | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | **Staff** | The team members linked to this resource, such as the coach who gives these lessons. | | **Security deposit in … (optional)** | An amount in credits held for each booking and refunded afterwards. See [Club settings](/en/guide/admins/club-settings). | | **Maximum occupants** | How many people the resource holds, from 1 to 51. | | **Default check-in (minutes before)** | New reservations get a check-in time this many minutes before their start. | | **Default check-out (minutes after)** | New reservations get a check-out time this many minutes after their end. | | **Cancellation notice (days before)** | Members who cancel later than this are not refunded the price. | ## 3. Set its prices [#3-set-its-prices] 1. Enter the price in your club's [credits](/en/guide#credits) in **Default price in …**, for example **Default price in Stars ⭐**. It applies to [plans](/en/guide#plan) without a custom price. 2. In **Charged**, choose **Per hour**, **Per day**, **Per night** or **One-off**. 3. To give a membership plan its own price, select **Add plan price**, choose the plan and enter its price. Remove it with its bin. 4. If the category is open to non-members, turn on **Bookable externally** and enter the price in euros in the **Non-members** line. Non-members pay it by card. 5. Select **Save**. To create your plans and name your credits, see [Plans and pricing](/en/guide/admins/plans-and-pricing) and [Payments](/en/guide/admins/payments). ## 4. Sell products with a resource [#4-sell-products-with-a-resource] The **Products** tab of a resource lists what can be added to a booking, such as a bottle of champagne in a suite. <figure> <img src="/images/guide/admins/resource-products.png" alt="Products tab of the Penthouse scrolled to the Champagne bottle open for editing: its image with a Remove image button, the name Champagne bottle, a price of 65 Stars, a description, Minibar consumption selected as the billing, a Done editing button outlined in orange, and Add product, Close and Save buttons." width="1280" height="800" /> <figcaption> The example minibar of the Penthouse suite. </figcaption> </figure> 1. Select **Add product**, or **Edit** on an existing one. 2. Select **Upload image** and pick a PNG or JPEG picture of 500 KB at most. The image is required. 3. Type the **Product name**, its **Product price** in credits and, if you like, a **Description (optional)**. 4. Under **How is this product billed?**, choose: * **Reservation add-on**: customers or staff add it to the reservation. * **Minibar consumption**: housekeeping reports what was consumed. It is not offered when booking. 5. Select **Done editing**, then **Save**. To take a product off the list, open it and select **Remove product**. With housekeeping turned on, the resource also has **Checklist**, **State**, **Consumption** and **History** tabs. See [Housekeeping](/en/guide/staff/housekeeping). # Sessions, guest lists and blocked periods Schedule the sessions of a group class or an event, keep some for a guest list, and close periods for maintenance. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/sessions-and-blocked-periods In a category booked by session, such as a group class or an event, members join the sessions you schedule. This page shows how to schedule sessions, keep some for chosen members, and close a category or a resource for a while. To create the category and choose how members book, see [Set up reservation categories](/en/guide/admins/reservation-categories). ## 1. See the sessions of a category [#1-see-the-sessions-of-a-category] In the left menu, select **Reservations** and select the category in the strip. Then select **Manage sessions** at the top of the calendar. A warning icon on the button means no session is scheduled yet. <figure> <img src="/images/guide/admins/manage-sessions.png" alt="Manage sessions window on the Recurring tab for Cours collectif: under Every day, Open studio at 12:30 for 45 minutes in the Main studio marked Open access, then sessions grouped by day, such as Sunday and Monday at 09:00, 15:00 and 18:00, each lasting 60 minutes in the Main studio with 8 places, each with a bin button, and Close and Add session buttons, Add session outlined in orange." width="1280" height="800" /> <figcaption> The sessions of the example class Cours collectif, with the daily Open studio that members attend without booking. The control to use is outlined in orange. </figcaption> </figure> * **Recurring** lists the sessions that repeat every week or every day. Each line shows the location, then the number of places, **No member limit** or **Open access**, and **Guest list** when the session is kept for some people. * **Dated** lists the one-off sessions. Select **Show past sessions** to see earlier ones. Select the pencil on a dated session to change it. * Select the bin on a session to delete it, then confirm with **Delete**. ## 2. Add a session [#2-add-a-session] Select **Add session**. 1. Under **When**, choose **One-off**, **Every week** or **Every day**, then the **Day** or the **Start date**, the **Start time** and the **End time**. A session can end after midnight: the window then shows **Ends the next day.** 2. Under **Details**, type a **Session title (optional)** and choose the **Location**, the resource where it takes place. 3. Under **Access**, choose the **Booking**: * **Reservation required**: members book a place. Set a **Member limit (optional)** to cap the number of places. * **Open access**: members come without booking. The session still shows in their planning. 4. Choose **Who can book**: **Every member**, or **Guest list** to keep the session for some people. With a guest list, add **Membership plans**, **People**, or both. 5. Select **Add session**. <figure> <img src="/images/guide/admins/session-form.png" alt="Add session window for Cours collectif: Every week on Monday from 12:30 to 13:30, the title Founders' Pilates in the Main studio, Reservation required with a member limit of 10, Who can book set to Guest list with empty Membership plans and People fields, and the Add session button outlined in orange." width="1280" height="800" /> <figcaption> An example weekly class kept for a guest list. Add the [plans](/en/guide#plan) or people who can book before saving. </figcaption> </figure> ## 3. Keep a dated session for a guest list [#3-keep-a-dated-session-for-a-guest-list] To keep a dated session for some members, open it with its pencil on the **Dated** tab, choose **Guest list** under **Who can book** and add the plans or people. The window shows **This change applies only to this session.** Select **Save changes**. <figure> <img src="/images/guide/admins/session-guest-list.png" alt="Edit session window for Concert — soirée in the Event space, on a Saturday from 19:00 to 21:00: Reservation required with a member limit of 12, Who can book set to Guest list, and Membership plans showing 2 selected, the plans Adhésion 51 IÉNA (C) - Abonnement Cercle and Adhésion 51 IÉNA (CCS) - Abonnement Cercle + Sport, each with a remove button, and Cancel and Save changes buttons, Save changes outlined in orange." width="1280" height="800" /> <figcaption> The example evening concert of Événement is kept for two Cercle membership plans. </figcaption> </figure> Your team follows each session's participants, waitlist and attendance from the **Club calendar**. See [Use the club calendar](/en/guide/staff/planning). ## 4. Close periods and follow waitlists [#4-close-periods-and-follow-waitlists] * **Block periods**, at the top of the calendar in categories without sessions, closes all resources or one of them for a while, for example for maintenance. Nobody can book a blocked period. * **Waitlist** appears in categories booked by party size. It lists who is waiting for a place in the next 30 days. Both are explained in [Blocked periods, sessions and waitlists](/en/guide/staff/blocked-periods-and-waitlists). # Set up your club The order in which to set up a new club, from its settings to its team. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/set-up-your-club Some settings depend on others: [plans](/en/guide#plan) need a Stripe account, and an [application](/en/guide#application) form offers the plans you created. Follow these steps in this order to set up a new club. Each one links to the page that explains it in detail. ### Fill in the club settings [#fill-in-the-club-settings]Set the address, time zone, member fields and reservation rules. See [Club settings](/en/guide/admins/club-settings). ### Connect Stripe and name your credits [#connect-stripe-and-name-your-credits]Members pay online once your club has a Stripe merchant. See [Payments](/en/guide/admins/payments). ### Create your plans [#create-your-plans]Add memberships, credit packs and add-ons with their prices. See [Plans and pricing](/en/guide/admins/plans-and-pricing). ### Set up what members can book [#set-up-what-members-can-book]Create [reservation categories](/en/guide#reservation-category), then their resources, prices and sessions. See [Set up reservation categories](/en/guide/admins/reservation-categories), [Resources, prices and products](/en/guide/admins/resources-and-prices) and [Sessions, guest lists and blocked periods](/en/guide/admins/sessions-and-blocked-periods). ### Build your application form [#build-your-application-form]Choose the questions applicants answer and the steps they complete once accepted. See [Application forms](/en/guide/admins/onboarding-forms). ### Publish your club's website [#publish-your-clubs-website]Choose your club's web address and publish the application form on it. See [Your club's website](/en/guide/admins/hosted-site). ### Invite your team [#invite-your-team]Give each colleague access to the [back office](/en/guide#back-office) with the right permissions. See [Invite your team](/en/guide/admins/invite-your-team). # Your team's profiles and documents Keep the profile members see and the professional documents of each colleague up to date. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/team-documents Each colleague's staff file in **Team** holds the profile members see and their professional documents, such as a coach's professional card or diplomas. Select a colleague in **Team** to open their file: the **Profile** tab opens first. To find a colleague, see [Manage your team](/en/guide/admins/manage-your-team). ## 1. Edit the profile members see [#1-edit-the-profile-members-see] * **Biography** shows the name, photo and text members see when they talk to this colleague, for example a coach. Select **Edit** to change them, or **View member profile** to see the profile as members do. * **Qualifications and documents** holds their professional card, diplomas and other supporting documents. ## 2. Add a document [#2-add-a-document] To add a document: 1. Select **Add document**. 2. Choose the **Document type**: **Professional card**, **Diploma** or **Other document**. 3. Type a **Title**, and if you have them, the **Document number** and the **Issuing organization**. 4. Enter the **Issue date** and the **Expiry date**. The expiry date must be on or after the issue date. 5. Select **Choose file** and pick the scan: a PDF, JPEG or PNG file of 10 MB at most. 6. Select **Save document**. <figure> <img src="/images/guide/admins/team-add-document.png" alt="Staff file of Sport coach on the Profile tab with the document form: Document type Professional card, Title Sports instructor card, Document number CP-075-2024-1182, Issuing organization Préfecture de Paris, Issue date 15/01/2024, Expiry date 15/01/2029, a File area with a Choose file button, and Cancel and Save document buttons." width="1280" height="800" /> <figcaption> Adding an example professional card to the file of Sport coach. </figcaption> </figure> ## 3. Follow expiry dates [#3-follow-expiry-dates] Each document shows how long it stays valid: **Not expired**, then from 30 days before its expiry date a countdown such as **Expires in 20 days**, **Expires tomorrow** or **Expires today**, and finally **Expired**. Documents are grouped by type, with the number of documents next to each type. <figure> <img src="/images/guide/admins/team-documents.png" alt="Profile tab of Sport coach scrolled to the Diploma group with 2 documents: Pool lifeguard certificate (BNSSA) from the Ministry of Sports marked Expired, with its number, issue date, expiry date, date added and Preview, Download, Edit and Delete buttons, then First aid certificate (PSC1) from the French Red Cross marked Expires in 20 days." width="1280" height="800" /> <figcaption> The example coach's diplomas: one expired certificate and one that expires in 20 days. </figcaption> </figure> | Document type | Who sees it | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Professional card**, **Diploma** | **Member-visible details · Private file**: members see the type, title, issuing organization and dates under **Qualifications** on this colleague's profile. The file stays private. | | **Other document** | **Internal document**: only your team sees it. | On a document, select **Preview** or **Download** to open the file, **Edit** to change its details or **Replace file**, and **Delete** to remove it. Deleting asks you to confirm with **Delete document**. > Use the **Expires within 30 days** filter regularly to renew professional cards before they > expire. # Who sees what What each staff permission shows in the back office, to choose a colleague's access or find out why a section is missing. Source: https://docs.champagne.dev-ukab.fr/en/guide/admins/who-sees-what What a staff member sees in the [back office](/en/guide#back-office) depends on their [role](/en/guide#role) and, for the **Staff** role, on the permissions an admin ticked for them in each club. Use this page to choose a colleague's access, or to find which permission brings back a section or a button someone is missing. ## Roles [#roles] * **Owner** and **Admin** see every section and every button, in the clubs they are assigned to. * **Staff** see only what their permissions allow, in the clubs they are assigned to. A staff member with no permission in a club only sees their own account. * Only owners and admins see the **Team** section. * A module the platform [turned off for a club](/en/guide/admins/platform-administration#4-turn-modules-off-for-a-club) is hidden from everyone in that club, owners and admins included, whatever their permissions. A suspended club disappears from everyone's back office. Admins set permissions when they [invite a colleague](/en/guide/admins/invite-your-team) and change them later from [the team list](/en/guide/admins/manage-your-team). A change applies as soon as it is saved. ## Permissions [#permissions] Some permissions include another one: ticking **Create and edit member profiles**, for example, also gives **View member profiles**. The "Also gives" column lists them. ### Members and applications [#members-and-applications] | Permission | What it shows | Also gives | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | **View member profiles** | The **People** section and member profiles, read only. | | | **Create and edit member profiles** | **Add person** and guest invitations in **People**. On a profile: **Edit information**, the photo, notifications, plan prices and credits, and **Sign out member**. | **View member profiles** | | **Edit staff notes** | Writing the internal note on a profile. | **View member profiles** | | **Suspend and ban members** | The status card on a profile: suspend, block bookings, ban, disable and give access back. | **View member profiles** | | **Manage member tags** | Adding and removing tags on a profile. | **View member profiles** | | **Manage memberships** | Granting and removing plans on a profile. | **View member profiles** | | **Review applications** | The **Applications** section, and the link from a profile to the person's application. | | | **Write and send newsletters** | The **Newsletters** section. | | ### Daily operations [#daily-operations] | Permission | What it shows | Also gives | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------- | | **View assigned planning** | The **Club calendar**, for the reservation categories assigned to the person. | | | **Create and change reservations** | Creating reservations from the calendar, changing, moving and cancelling them, cancelling sessions and removing participants. | **View assigned planning** | | **Validate attendance and tickets** | Checking tickets at the door, recording departures and marking attendance. | **View assigned planning** | | **Manage reservation messages** | The messages of reservations that need a conversation, in **Chats** and on the reservation. | **View assigned planning** | | **Chat with customers** | **Chats** with members and customers, and **New chat** on a profile. | **View member profiles** | | **Manage housekeeping and consumption** | The **Housekeeping** section, for categories that use housekeeping. | **View assigned planning** | | **Manage their own availability** | **My availability**, to set the hours the person can work. | | ### Finance [#finance] | Permission | What it shows | Also gives | | ------------------------------------ | --------------------------------------------------- | ---------- | | **Manage invoices and credit notes** | The **Finance** section: invoices and credit notes. | | ### Club configuration [#club-configuration] | Permission | What it shows | Also gives | | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------- | | **Configure plans and reservation resources** | The **Plans** and **Reservations** sections: plans, reservation categories, resources, floor plans and blocked periods. | **View assigned planning** | | **Manage club settings and integrations** | The **Settings** section, the application forms in **Applications**, connecting invoicing in **Finance** and managing tags. | | ## Without a permission [#without-a-permission] * **My reservations** shows for staff members who can be booked themselves, such as coaches, with no permission needed. * Creating a reservation from the calendar needs both **Create and change reservations** and **View member profiles**, to choose who the reservation is for. * A staff member with **View assigned planning** but no assigned [reservation category](/en/guide#reservation-category) has no **Club calendar**: ask an admin to tick their categories in **Accessible planning categories**. ## Restrictions [#restrictions] Two switches in a team member's **Access** tab, under **Restrictions**, limit an account whatever its role and permissions. They apply to the whole account, in every club. * **Read only**: the account sees everything its role and permissions allow and cannot change anything, apart from its own name, password and profile. * **Hide personal data**: people appear under a pseudonym such as Member 4F2A1C, the same on every screen. Contact details, photos, notes, documents and messages are hidden, and searching people by name is unavailable. Ticking both for an admin gives a technical team access to your club without letting them change it or learn who your members are. The space owner cannot be restricted. # When your account is limited Understand why you cannot book or see some pages, and what you can still do. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/account-status Your club can limit your account, for example after missed bookings, and some pages need a valid membership. This page explains the notices you then see in your member area. For the rest of the member area, see [Your member area](/en/guide/members/your-account). ## 1. Read your account status [#1-read-your-account-status] When your club limits your account, the status next to your name changes, and an **Account status** card at the top of every page explains it. When the club gave a reason, it appears under **Reason**. <figure> <img src="/images/guide/members/account-suspended.png" alt="Member area of Camille Roussel with the Suspended status, an Account status card reading Your access is suspended through 31 December 2026 with the reason Demo: temporary access suspension, and only the Overview and Billing tabs." width="1280" height="800" /> <figcaption> The example member Camille Roussel, suspended until the end of the year. </figcaption> </figure> | Status | What the card says | What you can still do | | -------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------ | | **Suspended** | **Your access is suspended through** followed by a date | See your overview, billing and inbox. Reservations and invitations are hidden. | | **Bookings blocked** | **You cannot make reservations through** followed by a date | Everything except new bookings. Your existing bookings stay available. | | **Banned** | **Your account has been banned.** | See your overview, billing and inbox. | | **Disabled** | **Your account is currently disabled.** | See your overview, billing and inbox. | Contact your club if you think a status is wrong: only the club can change it. ## 2. When you cannot book [#2-when-you-cannot-book] When your bookings are blocked, the **Reservations** tab shows **Your bookings are currently blocked** in place of the club schedule. Your upcoming bookings are listed below it. <figure> <img src="/images/guide/members/bookings-blocked.png" alt="Warning reading Your bookings are currently blocked. Your existing reservations remain available below." width="1056" height="68" /> <figcaption> The notice of a member whose bookings are blocked after repeated no-shows. </figcaption> </figure> Without a valid membership, the **Reservations** tab shows **A valid membership is required to book** in place of the club schedule. Select **View my membership** to choose a [plan](/en/guide#plan), see [Your membership](/en/guide/members/membership-and-credits). <figure> <img src="/images/guide/members/membership-required.png" alt="Warning reading A valid membership is required to book. Check your membership before making a new reservation. Your existing reservations remain available below, with a View my membership button outlined in orange." width="1056" height="100" /> <figcaption> The Reservations tab of Louis Boulanger, who has no membership yet. </figcaption> </figure> ## 3. Guest and temporary accounts [#3-guest-and-temporary-accounts] Guests and visitors see one more card: * **Guest access** tells a guest invited by a member until when they can use the club, for example **You are a guest of** followed by the member's name and the end date. The temporary account closes automatically afterwards. See [Accept an invitation you received by email](/en/guide/members/invitations-from-email). * **Temporary account** tells a visitor who booked online that the account ends at the end of a given day. See [Book online as a visitor](/en/guide/members/book-online). <figure> <img src="/images/guide/members/guest-account.png" alt="Member area of Clara Fontaine with the Active and Guest account labels, a Guest access card saying that Clara is a guest of Alex Morgan until her access end date and that her temporary account then closes automatically, the Overview and Reservations tabs, the I'm in the building card and a Next reservation card reading Nothing booked." width="1280" height="800" /> <figcaption> Clara Fontaine, visiting Ukab Clubs as Alex Morgan's guest. </figcaption> </figure> # After the club's decision Read the club's answer, pay and sign what is left after approval, and open your member area. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/after-your-application Once you send your [application](/en/guide#application), the club reviews it and emails you when it has decided. Open your application to see the result. To fill in and send your application, see [Complete and sign your application](/en/guide/members/sign-your-application). ## 1. If the club rejects your application [#1-if-the-club-rejects-your-application] If the club rejects your application, it shows **Application not accepted**, with the club's message under **Reason from the club** when it left one. Contact the club if you have questions. <figure> <img src="/images/guide/members/application-rejected.png" alt="Application of Maya Demo with the Rejected status, the heading Application not accepted and a reason from the club." width="1152" height="360" /> <figcaption> A rejected example application with the club's reason. </figcaption> </figure> ## 2. Finish the steps after approval [#2-finish-the-steps-after-approval] If the club accepts your application, the page shows **Your application is accepted** and the steps left to do after approval, each marked **Completed**, **To do** or **Upcoming**. The **Next action · Step …** card on the right opens the current step: * A payment step shows your [plan](/en/guide#plan) and its price. Select the **Pay … with Stripe** button, which shows the amount. Stripe, the secure payment service, opens to take your payment, then brings you back to your application. * A signature step works as described in [Sign a document](/en/guide/members/sign-your-application#2-sign-a-document). * An address step asks for your postal address, and a phone step for your mobile number and the code texted to it. Both are saved to your profile. * An identity step asks for your ID card, passport or driving licence, and often a selfie. Select **Verify my identity**: Stripe, the club's verification service, opens to photograph them, then brings you back. While Stripe checks them, the step shows **Stripe is checking your document**; the page updates by itself. * A visit step lets you book a first visit with the club. Choose a **Date**, select a time, then **Book this time**. Full times are crossed out. Until you select **Continue**, you can **Change the time** or **Cancel the booking**. The booked time also shows under the step in the list, and you receive the usual confirmation email and reminder. ### If something goes wrong [#if-something-goes-wrong] | Message | What to do | | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | **Signature failed**, with **Yousign could not complete the signature. Please try again.** | Select **Continue to Yousign** to sign again. | | **You declined the signature. You can start again when you are ready.** | You refused to sign in Yousign. Select **Continue to Yousign** when you are ready. | | **Your signature is not confirmed yet. Finish signing in Yousign, then check again.** | Finish signing in the Yousign tab, then select **Check signature**. | | **Checkout cancelled**, **No payment was confirmed. You can try again.** | You left the Stripe page without paying. Select the **Pay … with Stripe** button again. | | **Checkout expired. Start a new payment.** | The payment page was open too long. Select **Start a new payment**. | | **Payment is currently unavailable. Please contact the club to complete this step.** | The club cannot take the payment online yet. Contact it. | | **We couldn’t identify your payment. Please contact the club before trying again.** | Do not pay twice: ask the club whether your payment arrived. | | **Your identity could not be verified**, with a reason from Stripe | Follow the reason, for example use a valid document, then select **Try again**. | | **You do not meet the minimum age** | Your document shows you are under the club's minimum age. Contact the club if this is a mistake. | | **Confirm your email to open this application.** | The link opened in a browser that does not know you. Select **Find my application** to get a code. | Coming back from Stripe or from a link in an email, your application can open in a tab that has no access to it. If it says **Your payment has been received. Find your application with your email to continue.**, select **Find my application** and confirm your email to carry on. ## 3. Open your member area [#3-open-your-member-area] When all steps are done and you have no account yet, choose your password in **Choose your password** and select **Save password and open my account**, or **Save password and sign in** on the club's own application website. See [Activate your account and reset your password](/en/guide/members/passwords-and-activation). Your member area then opens, or its sign-in page when you applied on the club's website. When you reopen your application later, it shows **Welcome to the club**: select **Go to my account** to open your member area. <figure> <img src="/images/guide/members/application-welcome.png" alt="Application of Sami Bernard with the Accepted status, the heading Welcome to the club, a Go to my account button outlined in orange, a Forgot password link and one completed step." width="1152" height="460" /> <figcaption> The completed application of the example member Sami Bernard. </figcaption> </figure> # Billing Pay what you owe and download your invoices. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/billing Your club sends invoices for your membership and other charges. Open **My account**, select the **Billing** tab, then **Invoices** to pay what is due and download your invoices. ## 1. Pay an outstanding invoice [#1-pay-an-outstanding-invoice] **Outstanding invoices** lists what you still have to pay, with the amount left and the due date. <figure> <img src="/images/guide/members/billing.png" alt="Invoices page of Sam Rivera: an outstanding invoice EFAVXZ3T-0007 of €4,500.00 past its due date with Open PDF and Pay on Stripe buttons, Pay on Stripe outlined in orange, and the same invoice in My invoices marked Overdue." width="1280" height="800" /> <figcaption> An overdue example membership invoice for Sam Rivera. The control to use is outlined in orange. </figcaption> </figure> 1. Select **Pay on Stripe**. A secure payment page opens in a new tab. 2. Pay the invoice by card. 3. Come back to your account. A payment may take a moment to appear: the invoice then leaves **Outstanding invoices**. If an invoice shows **Contact your club to pay.**, it cannot be paid online: ask your club how to settle it. ## 2. Find and download your invoices [#2-find-and-download-your-invoices] **My invoices** lists all your invoices, the most recent first, with their number, **Total**, **Issued** date and status. Select **Open PDF** to open an invoice in a new tab, then download or print it from there. **PDF unavailable** means no PDF can be opened for this invoice. Use **Previous** and **Next** when you have many invoices. <figure> <img src="/images/guide/members/billing-paid.png" alt="Invoices page with no outstanding invoices and one paid invoice EFAVXZ3T-0003 of €1,800.00 with its issue date and an Open PDF button outlined in orange." width="1056" height="348" /> <figcaption> The paid example membership invoice of Jamie Chen. </figcaption> </figure> | Status | What it means | | -------------------- | -------------------------------------------------------- | | **Awaiting payment** | The invoice is waiting for your payment. | | **Overdue** | The due date has passed and the invoice is still due. | | **Paid** | The invoice is fully paid. | | **Draft** | Your club is still preparing the invoice. | | **Voided** | The club cancelled the invoice: you have nothing to pay. | | **Uncollectible** | The club no longer expects this invoice to be paid. | If the page says **Invoices are currently unavailable. Contact your club.**, the club's online billing is not set up: ask the club for your invoices. > Bookings are paid from their own page with **Pay reservation**, see [Book a > reservation](/en/guide/members/book-a-reservation). Credit purchases appear in your credit history, > see [Your credits](/en/guide/members/credits). # Book a reservation Pick an activity in your club's schedule, choose a time and confirm your booking. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/book-a-reservation Members book rooms, places and sessions from their member area. You need an active membership at your club to book. Without an account, use **Book online** to book as a visitor: see [Book online as a visitor](/en/guide/members/book-online). ## 1. Open your reservations [#1-open-your-reservations] Sign in to the [member app](/en/guide#member-app), open **My account** and select the **Reservations** tab. The top of the page lists **My upcoming reservations**. Select a booking to see its details, pay it or message your club. <figure> <img src="/images/guide/members/reservations.png" alt="Reservations tab of Alex Morgan's member area listing upcoming bookings with their date, status and price: a Cours collectif class with the label Waitlist · #3 and a Leave button, a Suite d'hôtel marked Booked — payment due with a Pay reservation button outlined in orange, a Place à la piscine booking also marked Booked — payment due with its own Pay reservation button, and the start of a Cours particulier booking." width="1280" height="800" /> <figcaption> Upcoming reservations of the example member Alex Morgan at Ukab Clubs. The control to use is outlined in orange. </figcaption> </figure> Each booking shows a status and its price, with the **Security deposit** when the club asks for one. **Booked — payment due** means the place is held for you and still needs to be paid: select **Pay reservation** to settle it. A lesson whose time the club proposed shows **Review proposal** instead. Without a valid membership, the page shows **A valid membership is required to book** in place of the club schedule. See [When your account is limited](/en/guide/members/account-status). ## 2. Find a free time in the club schedule [#2-find-a-free-time-in-the-club-schedule] Scroll down to **Club schedule**. 1. In **Activity**, pick what you want to book, for example a meeting room. 2. Use the arrows or **This week** to move between weeks. 3. If the activity has several rooms or places, pick one in **Resource**. Each day shows the times already taken as **Unavailable**. Select **Choose a time** on the day you want. <figure> <img src="/images/guide/members/club-schedule.png" alt="Club schedule with the Salle de réunion activity and Conference room resource, showing a week where each day has Unavailable slots and a Choose a time button, one of them outlined in orange." width="1056" height="518" /> <figcaption> A week of the example meeting room, Salle de réunion. </figcaption> </figure> Some activities work differently. Group classes list their sessions directly, so you register for a session. Places on a floor plan, such as pool loungers, show **Choose a place** so you can pick your spot on the map. ## 3. Confirm your booking [#3-confirm-your-booking] A form opens with the activity and resource you chose. Set the **Number of people**, the **Start date** and **Start time**, and the **End date** and **End time**. The **Total** shows the price before you confirm. <figure> <img src="/images/guide/members/booking-form.png" alt="Booking form for the Conference room with number of people, start and end date and time, a Free total and a Book reservation button outlined in orange." width="448" height="493" /> <figcaption> The booking form for the example Conference room, free for members. </figcaption> </figure> Some activities offer extras, such as drinks brought to your pool lounger. They are listed under **Products** with their price: set a **Quantity** for each one you want. Their cost is added to the total. <figure> <img src="/images/guide/members/booking-products.png" alt="Products section of a booking form with Sparkling water, served chilled, 50 cl, at 3.5 Stars, and a Quantity of 2." width="416" height="117" /> <figcaption> Two sparkling waters added to an example pool booking. </figcaption> </figure> Below the dates, a summary sums up what you will pay: | Line | What it means | | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | **Reservation** and **Refundable deposit** | The price and the security deposit, shown separately when the club asks for one. | | **Total** or **Total including deposit** | What the booking costs in all. | | **Available balance** | Your credits, when the booking is paid in credits. | | **Payment required to confirm.** | The booking is held until you pay it. | | **Subject to club approval.** | The club confirms the booking before it is final. | | **Free cancellation until …** | Cancel before this date to get the price back. | | **The price is non-refundable: the cancellation notice has passed.** | Cancelling now does not give the price back. | | **Missing … for payment.** | Your balance is too low to pay the booking right away. | <figure> <img src="/images/guide/members/booking-cost.png" alt="Booking summary with a Total of 37 Stars, an Available balance of 540 Stars and the note Payment required to confirm." width="416" height="81" /> <figcaption> The summary of the example pool booking: 30 Stars for the place and 7 Stars of water. </figcaption> </figure> When your balance is too low, the summary shows **Missing … for payment.** with the amount you lack. If the booking does not need the club's approval, it adds **You can book now and pay once your credits are topped up.**: you can still select **Book reservation** and pay later. Select **Top up my credits** to open your [credits](/en/guide#credits) page and buy a pack. See [Your credits](/en/guide/members/credits#1-top-up-your-credits). <figure> <img src="/images/guide/members/booking-shortfall.png" alt="Booking summary of a suite with a Reservation of 250 Stars, a Refundable deposit of 500 Stars, a Total including deposit of 750 Stars and an Available balance of 540 Stars, then the warning Missing 210 Stars for payment, the note You can book now and pay once your credits are topped up, and a Top up my credits button outlined in orange." width="352" height="333" /> <figcaption> A one-night suite booking costs Alex Morgan 750 Stars with its deposit, 210 more than his balance. </figcaption> </figure> Select **Book reservation**. The booking appears in **My upcoming reservations**. If it has a price, pay it from there with **Pay reservation**. See [Your reservations](/en/guide/members/reservations-and-coaching#3-pay-a-reservation). > Some clubs price bookings in Stars, the club's credits. You buy Stars in packs from your account. > See [Your credits](/en/guide/members/credits). # Book online as a visitor Book a room or a place at a club without a membership and pay online. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/book-online Some clubs let visitors book online, for example a hotel suite for a few nights. You do not need a membership: you choose what to book, enter your details and pay by card. Champagne creates a temporary account for your stay so you can follow your booking. ## 1. Choose a club [#1-choose-a-club] Select **Book online** in the top navigation of the [member app](/en/guide#member-app). The page lists the clubs open to online booking. Select the club you want. <figure> <img src="/images/guide/members/book-online-clubs.png" alt="Book online page listing three clubs open to online booking: Astel, Ukab Clubs and Vosges, with Ukab Clubs outlined in orange." width="1280" height="800" /> <figcaption> The demo clubs Astel, Ukab Clubs and Vosges, all open to online booking. The control to use is outlined in orange. </figcaption> </figure> When only one club takes online bookings, its page opens straight away. If no club takes them yet, the page says **No club offers online booking yet.** ## 2. Choose what to book and when [#2-choose-what-to-book-and-when] The club's page shows what you can book. When the club offers several activities, select one of the tabs at the top. 1. In **Resource**, pick a room or a place. Each line shows its price, how many people it holds and, when the club asks for one, its security deposit. A resource already taken for your dates shows **Unavailable for this period**. 2. Set the **Number of people** when the resource holds more than one person. <figure> <img src="/images/guide/members/book-online-resource.png" alt="Book online page of Ukab Clubs for Suite d'hôtel listing four suites at €320.00 per day for up to 4 people with a €50.00 security deposit, Courtyard suite selected and outlined in orange, and the Number of people set to 2." width="768" height="588" /> <figcaption> Choosing the example Courtyard suite for two people. The control to use is outlined in orange. </figcaption> </figure> 3. Set the **Start date** and **Start time**, and the **End date** and **End time**. Some activities have a fixed length: you then only choose the start. 4. Check the **Total** and select **Continue**. <figure> <img src="/images/guide/members/book-online-selection.png" alt="Start and end dates and times of the booking, from 10:00 on the start date to 10:00 two days later, with a Total of €1,010.00, the line Including a refundable security deposit of €50.00 and a Continue button outlined in orange." width="768" height="364" /> <figcaption> The example Courtyard suite at Ukab Clubs, booked for two people over three days: €960.00 for the stay plus the €50.00 deposit. </figcaption> </figure> **Continue** becomes available once the resource is free and the dates are valid. Under the dates, the form tells you when the end is before the start, when the start is in the past, or when the stay is longer than 62 days. Without an account, a stay lasts at most 31 days: a longer one is refused at the last step. When the resource has a security deposit, the **Total** includes it and the line **Including a refundable security deposit of …** under it gives the refundable amount. To change your choice after **Continue**, select **Change**. ## 3. Enter your details [#3-enter-your-details] Under **Your details**, two tabs let you choose how to continue: * **Book without membership** creates a temporary account for this booking. * **Sign in** is for members of this club. ### Book without membership [#book-without-membership] 1. Enter your **First name**, **Last name**, **Email** and **Phone**. 2. Choose a **Password**: at least 12 characters, with a lowercase letter, an uppercase letter, a digit and a symbol. You use it to sign in again during your stay. Select the eye icon to show or hide it. 3. Select **Continue to payment**. <figure> <img src="/images/guide/members/book-online-details.png" alt="Your details section with the Book without membership tab selected: the note Your temporary account ends after your stay, First name Camille, Last name Martin, an example email and phone, a hidden password and a Continue to payment button outlined in orange." width="688" height="489" /> <figcaption> The temporary account form filled in for the example visitor Camille Martin. </figcaption> </figure> Champagne books the resource, signs you in with your temporary account and opens the payment page. If the email already has an account at this club, a message asks you to sign in instead: use the **Sign in** tab. ### Sign in as a member [#sign-in-as-a-member] Select **Sign in**, enter your **Email** and **Password**, then select **Sign in**. The page then shows the member booking form for the same activity, with your choice kept. You book as in [Book a reservation](/en/guide/members/book-a-reservation). If your membership does not allow you to book, the page says that a valid membership is required. ## 4. Pay and follow your booking [#4-pay-and-follow-your-booking] After **Continue to payment**, you pay by card on the secure payment page. When you come back, your booking opens in your temporary account: | Message | What it means | | --------------------------------------------------------------------- | ---------------------------------------------------------------- | | **Payment received — your reservation is confirmed.** | Your payment went through and the booking is confirmed. | | **Confirming your payment…** | The payment is being checked. The page updates on its own. | | **Payment canceled. Your reservation stays booked until it is paid.** | You left the payment page. Pay from the booking page when ready. | If the payment page cannot open, a message says that your booking is saved: pay it from your account. When the booking costs nothing, it opens directly without a payment step. Your temporary account shows a **Temporary account** notice with the date it ends: at the end of the last day of your stay. It is then deleted, and a booking still waiting for payment is canceled. Until then, sign in with your email and password from **My account** to see your booking and pay it. If you open **Book online** again while signed in with it, the page says **You are signed in with a temporary account.** and offers **My reservations**. <figure> <img src="/images/guide/members/book-online-account.png" alt="Member area of Marc Lefort with the Active and Temporary account labels, a Temporary account card giving the last day of the temporary account, only the Overview and Reservations tabs, the I'm in the building card and a Next reservation card for an upcoming Suite d'hôtel stay marked Booked — payment due." width="1280" height="800" /> <figcaption> The temporary account of the visitor Marc Lefort, who booked the Penthouse for two nights. </figcaption> </figure> The temporary account has two tabs: **Overview** and **Reservations**. Open your booking to see its price, the security deposit and the date until which you can cancel for free, then select **Pay … by card**. See [Your reservations](/en/guide/members/reservations-and-coaching#2-read-the-money-details). > If the club does not take online bookings, its page says **Online booking is not available for > this club.** Select **View available clubs** to go back to the list. If the page says **Online > booking is unavailable. Please try again.**, reload it a little later. <figure> <img src="/images/guide/members/book-online-closed.png" alt="Book online card with the message Online booking is not available for this club and a View available clubs button outlined in orange." width="688" height="164" /> <figcaption> The page of a club that does not take online bookings. </figcaption> </figure> # Your credits Top up your club credits, follow your credit history and buy add-ons. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/credits [Credits](/en/guide#credits) are the club's own currency for bookings: each club picks its name, for example Stars ⭐ at Ukab Clubs. They live in the **Billing** tab of **My account**. For your membership, see [Your membership](/en/guide/members/membership-and-credits). ## 1. Top up your credits [#1-top-up-your-credits] Select **Credits**. **My credits** shows your balance. Under **Top up**, each pack shows how many credits you get and its price. **Save** shows the discount compared with buying credits one by one. <figure> <img src="/images/guide/members/credits.png" alt="My credits card of Alex Morgan with a balance of 540 Stars, three packs with their Top up for buttons, the first one outlined in orange: Discovery 250 Stars for €25.00, Weekend 600 Stars for €50.00 saving 16% and Regular 1,250 Stars for €100.00 saving 20%, and a Credit history with a Weekend top-up, three reservations and a refund." width="1056" height="804" /> <figcaption> Alex Morgan's balance of 540 Stars and credit history at Ukab Clubs. </figcaption> </figure> Select **Top up for …** on a pack and pay on the secure page that opens. Back on the page, **Credits added.** confirms the top-up. **Top-up submitted. Awaiting payment confirmation.** means the payment is still being checked. **Top-up canceled.** means you left the payment page without paying: no credits were added. When a booking costs more credits than you have, the booking form and the booking's page offer **Top up my credits**. When you come from the booking's page, select **Return to my reservation** after the top-up to finish paying it. ## 2. Follow your credit history [#2-follow-your-credit-history] **Credit history** lists every movement, the most recent first, with what it was for, its date, its amount and, for a purchase, the price paid. In the example above, Alex bought the Weekend pack for €50.00, spent 30 Stars on Transat #2, got the 80 Stars of a cancelled Pilates lesson back, then spent 30 Stars on Transat #4. Use **Previous** and **Next** to browse older movements. | Entry | What happened | | -------------------- | --------------------------------------------- | | **Top-up** | You bought a credit pack. | | **Reservation** | Credits were spent on a booking. | | **Refund** | Credits came back after a cancelled booking. | | **Security deposit** | Credits were held as a deposit for a booking. | | **Deposit returned** | The deposit came back to your balance. | | **Add-on** | Credits were spent on an add-on. | > Some clubs sell extras, called add-ons. They appear under **Add-ons** on the **Membership** page > with their price in credits. Select **Buy for …**: the credits are taken from your balance right > away and the button shows **Purchased**. The add-ons you own are listed under **My add-ons**. # Find a club and apply Choose a club, read its offers and start an application. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/find-a-club-and-apply You join a club by sending it an [application](/en/guide#application) from the [member app](/en/guide#member-app). You do not need an account to apply: the club creates it for you once it accepts your application. ## 1. Pick a club [#1-pick-a-club] Open the member app on the **Clubs** tab. In **Club**, type the name of the club or open the list and select it. The club's profile appears with its address and description. <figure> <img src="/images/guide/members/clubs.png" alt="Clubs tab with Ukab Clubs selected in the Club field, outlined in orange, its profile card with address, description and a Book online button, and the start of the Onboardings section." width="1280" height="800" /> <figcaption> The profile of the example club Ukab Clubs. The control to use is outlined in orange. </figcaption> </figure> If the club takes bookings from visitors, its profile shows **Book online**. You can use it to book without being a member. ## 2. Compare the onboardings [#2-compare-the-onboardings] An onboarding is one way to join the club, with its own form and its own [plans](/en/guide#plan). A club may offer several, for example one for regular members and one for founding members. Under **Onboardings**, each card shows the number of steps and lists them in order, with the kind of answer each step expects and whether it is **Optional**. <figure> <img src="/images/guide/members/club-onboardings.png" alt="Onboardings section with two cards, Common members with 7 steps and Founding members with 4 steps, each listing its steps and showing an Apply as button and an Open hosted page button, Apply as Common members outlined in orange." width="1072" height="476" /> <figcaption> The two example onboardings of Ukab Clubs. </figcaption> </figure> | Step kind | What you do | | --------------------------------------------- | -------------------------------------------------------- | | **Name** | Type your first and last name. | | **Email verification** | Type your email address and the code the club sends you. | | **Short answer**, **Long answer**, **Choice** | Answer the club's question. | | **Offer selection** | Pick the membership plan you want. | | **Sponsors** | Type the sponsor codes members of the club gave you. | | **Address** | Type your postal address. | | **Phone verification** | Type your mobile number and the code texted to it. | | **Identity verification** | Have your ID document checked by Stripe. | | **Visit booking** | Book a first visit with the club. | | **Signature** | Sign a document online, such as the club rules. | | **Payment** | Pay your plan online. | Below the onboardings, **Membership offers** lists the club's plans and add-ons with their price. You pick your plan during the application. <figure> <img src="/images/guide/members/club-offers.png" alt="Membership offers section with three yearly subscriptions of Ukab Clubs priced 1,800, 4,500 and 6,300 euros per year." width="1072" height="252" /> <figcaption> The example yearly subscriptions of Ukab Clubs. </figcaption> </figure> ## 3. Start your application [#3-start-your-application] Select **Apply as** followed by the name of the onboarding, for example **Apply as Common members**. Some onboardings first show a short presentation page: select **Start application** to continue. Your application opens on its first step. To fill it in, see [Complete and sign your application](/en/guide/members/sign-your-application). > A card marked **Hosted** can also be filled in on the club's branded application website. Select > **Open hosted page** to go there. Clubs set this website up in [Your club's > website](/en/guide/admins/hosted-site). ## 4. Find your application again [#4-find-your-application-again] Your answers are saved at each step, so you can stop and come back later. To reopen an application, or to follow one you already sent: 1. Select the **Applications** tab, or **Find my application** at the bottom of the **Clubs** tab. 2. Check the **Club** and type the **Email address** you used in your application. 3. Select **Send code**. The club emails you a six-digit code, valid for 10 minutes. 4. Type the code in **Six-digit verification code** and select **Find applications**. 5. Select **Open application** next to the application you want. <figure> <img src="/images/guide/members/find-my-application.png" alt="Find my application card in the middle of the page with Ukab Clubs in the Club field, an example email address and a Send code button outlined in orange." width="1280" height="800" /> <figcaption> Looking for the applications of an example email address at Ukab Clubs. </figcaption> </figure> If the email does not arrive, wait a minute and select **Resend code**, or select **Change email** to correct the address. # Get started From your application to your first booking, the steps of a new member in order. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/get-started Joining a club with Champagne takes a few steps. Follow them in this order: each one links to the page that explains it in detail. ### Apply to a club [#apply-to-a-club]Choose your club, compare its offers and start your [application](/en/guide#application). See [Find a club and apply](/en/guide/members/find-a-club-and-apply). ### Complete and sign your application [#complete-and-sign-your-application]Answer each step, sign the club's documents and send your application. See [Complete and sign your application](/en/guide/members/sign-your-application). ### Finish after the club's decision [#finish-after-the-clubs-decision]Once accepted, pay and sign what is left, then choose your password. See [After the club's decision](/en/guide/members/after-your-application). ### Open your member area [#open-your-member-area]Find your way around **My account** and update your profile. See [Your member area](/en/guide/members/your-account). ### Book your first reservation [#book-your-first-reservation]Top up your [credits](/en/guide#credits) if the club prices bookings in credits, then book. See [Your credits](/en/guide/members/credits) and [Book a reservation](/en/guide/members/book-a-reservation). Not a member and only want to book once? See [Book online as a visitor](/en/guide/members/book-online). # Guests and sponsorship Invite a guest to the club and sponsor someone who applies. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/guests-and-sponsorship As a member, you can open the club to people you know. Invite a guest for a few hours, or give a sponsor code to someone who applies to become a member. Both are in the **Invitations** tab of **My account**. ## 1. Invite a guest [#1-invite-a-guest] A guest gets a temporary account at your club, for example to join you for an afternoon. Select the **Invitations** tab, then **Club guests**. 1. Enter the **Guest email**. 2. Choose the **Access duration**: **12 hours**, **24 hours** or **48 hours**. The temporary account starts when the guest accepts. 3. Select **Send invitation**. The page confirms **Invitation sent to …** and the guest receives an email to accept it. <figure> <img src="/images/guide/members/guest-invitation.png" alt="Club guests page of Alex Morgan with the Invite to the club form filled with sophie@example.com and 48 hours selected, a Send invitation button outlined in orange, and My guest invitations listing guest@example.com as Invitation pending, with Clara Fontaine below it." width="1280" height="800" /> <figcaption> A new invitation for 48 hours to [sophie@example.com](mailto:sophie@example.com), below Alex Morgan's pending invitation. The control to use is outlined in orange. </figcaption> </figure> If the guest already has a pending invitation or active access, the form says **An invitation is already pending for this email.** or **This guest already has active access.**, and **Send invitation** stays unavailable. <figure> <img src="/images/guide/members/guest-already-invited.png" alt="Invite to the club form with guest@example.com, the note An invitation is already pending for this email, and a greyed-out Send invitation button. My guest invitations lists guest@example.com as Invitation pending, with 24 hours and the date the link expires, and Clara Fontaine as Guest access active, with the end of her access." width="1056" height="513" /> <figcaption> [guest@example.com](mailto:guest@example.com) already has a pending invitation from Alex Morgan, and Clara Fontaine is visiting as his guest. </figcaption> </figure> ## 2. Follow your invitations [#2-follow-your-invitations] **My guest invitations** lists the people you invited. Under each one, a line gives the access duration and when the link stops working, for example **24 hours · link valid until** followed by a date. Once the guest has accepted, it shows **Access until** and the end of their access. | Status | What it means | | ----------------------- | ------------------------------------------- | | **Invitation pending** | The guest has not accepted yet. | | **Invitation expired** | The guest did not accept in time. | | **Guest access active** | The guest is using their temporary account. | | **Guest access ended** | The access duration is over. | For an expired invitation or an ended access, select **Invite again**. The form fills in with the same email and duration: check them and select **Send invitation**. ## 3. Sponsor a new member [#3-sponsor-a-new-member] Some clubs ask applicants for codes from current members. Select **Sponsorship**. <figure> <img src="/images/guide/members/sponsor-codes.png" alt="Sponsor codes card with a Generate a code button outlined in orange, the code K7MZ-4QXP created a few days earlier and marked Available with Copy and Revoke buttons, Copy also outlined in orange, and the code H3RT-W9AB marked Used by Lucas Demo on 2 September 2026." width="1056" height="277" /> <figcaption> Alex Morgan's sponsor codes: one still available and one used by the applicant Lucas Demo. </figcaption> </figure> 1. Select **Generate a code**. A new code appears in the list as **Available**. 2. Select **Copy** and send the code to the person you sponsor. 3. They enter it in their [application](/en/guide#application), in a field such as **Sponsor code 1**. See [Complete and sign your application](/en/guide/members/sign-your-application). Each code works once. When it is used, it shows **Used** with the applicant's name and the date, as for H3RT-W9AB above. To cancel a code you no longer want to give, select **Revoke**, then **Revoke** again in **Revoke this code?**: the code stops working, even for someone you already gave it to. Only available codes can be copied or revoked. You can keep up to 5 codes that nobody has used yet. Beyond that, the page says **You can keep at most 5 unused sponsor codes. Revoke one first.** > You need a valid membership to sponsor. Without one, the page shows **A valid membership is > required to sponsor** and a **View my membership** link. # Accept an invitation you received by email Open a guest or reservation invitation from its email, and unsubscribe from a newsletter. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/invitations-from-email Clubs and their members send you emails with a link: an invitation to visit the club as a guest, an invitation to join someone's reservation, or a newsletter. Each link opens a page of the [member app](/en/guide#member-app) where you answer in a few clicks. ## 1. Accept a guest invitation [#1-accept-a-guest-invitation] A member or the club can invite you to the club for a few hours. You receive an email titled "You are invited to … as a guest". Open its **Accept the invitation** link before it expires: the email gives the number of days. The page **You are invited to …** says who invited you and for how many hours. Your email address is already filled in. 1. Enter your **First name** and **Last name**. 2. Choose a **Password** and type it again in **Confirm password**. Use at least 12 characters, with a lowercase letter, an uppercase letter, a digit and a symbol. You use it to sign in again while your guest access lasts. 3. Select **Accept invitation**. <figure> <img src="/images/guide/members/guest-invitation-accept.png" alt="You are invited to Ukab Clubs page: Alex Morgan invited you as a guest for 24 hours, the email guest@example.com, first name Robin, last name Guest, a password and its confirmation, and an Accept invitation button outlined in orange." width="400" height="584" /> <figcaption> An invitation from the example member Alex Morgan to [guest@example.com](mailto:guest@example.com), for 24 hours. The control to use is outlined in orange. </figcaption> </figure> Your temporary account starts when you accept, and your member area opens. It shows a **Guest access** notice with the time your access ends. The account closes automatically afterwards. If the page says **Invitation unavailable**, the link is incomplete, already used or expired. Ask the person who invited you to send a new invitation, or select **Back to home**. <figure> <img src="/images/guide/members/guest-invitation-unavailable.png" alt="Invitation unavailable card saying the guest invitation is invalid, already accepted or expired, with a Back to home link outlined in orange." width="400" height="197" /> <figcaption> A guest invitation link that no longer works. </figcaption> </figure> ## 2. Answer a reservation invitation [#2-answer-a-reservation-invitation] When a member adds you to their booking, you receive an email titled "… · Reservation invitation". Its link opens the **Reservation invitation** page with who invited you, the club, the place and the time of the booking. What you do next depends on the club: | What the page shows | What to do | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **Joining as …** with your name | Select **Accept invitation** to join, or **Decline**. | | A **Your name** field | Type your name, then select **Accept invitation**. Select **Decline** to refuse. | | **Create guest account** and **Decline** | The club asks guests to have an account. Select **Create guest account** and follow the guest invitation steps of section 1. | <figure> <img src="/images/guide/members/reservation-invitation.png" alt="Reservation invitation page: Alex Morgan invited you to join their reservation at Ukab Clubs, Transat #4, with the date and time, Joining as Jamie Chen, and Accept invitation and Decline buttons, Accept invitation outlined in orange." width="624" height="308" /> <figcaption> The example member Jamie Chen, invited by Alex Morgan to the pool place Transat #4. </figcaption> </figure> Once you answer, the page shows **Invitation accepted** or **Invitation declined**. If you have a member account at this club, the invitation also appears in your [reservations](/en/guide/members/tickets-stays-and-people#5-bookings-you-are-invited-to), where you can answer it too. The link works for 7 days, and never after the reservation ends. When it has expired, or the booking was canceled, the page says **Invitation unavailable**. <figure> <img src="/images/guide/members/reservation-invitation-unavailable.png" alt="Invitation unavailable card saying the invitation has expired or the reservation is no longer available, with a Back to home button." width="624" height="176" /> <figcaption> A reservation invitation link that no longer works. </figcaption> </figure> ## 3. Unsubscribe from a newsletter [#3-unsubscribe-from-a-newsletter] At the bottom of every club newsletter, select **Se désabonner / Unsubscribe**. The page asks **Unsubscribe from the newsletter?**: select **Unsubscribe** to confirm. <figure> <img src="/images/guide/members/newsletter-unsubscribe.png" alt="Unsubscribe from the newsletter? card with an Unsubscribe button outlined in orange." width="400" height="160" /> <figcaption> The page opened from the unsubscribe link of a newsletter. </figcaption> </figure> The page then says **You are unsubscribed** and names the club whose newsletters you no longer receive. If it says **This unsubscribe link is not valid**, open the full link from the newsletter email again. To manage the newsletter from your member area, see [Notifications](/en/guide/members/notifications). > To invite a guest yourself, see [Guests and sponsorship](/en/guide/members/guests-and-sponsorship). > To invite someone to your booking, see [Invite the people coming with > you](/en/guide/members/tickets-stays-and-people#4-invite-the-people-coming-with-you). # Lessons with a coach Follow a lesson request, accept the time the club proposes and read your coach's profile. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/lessons-with-a-coach Lessons with a coach, such as private lessons, start as a request without a fixed time. You agree on the time with the club and the coach in the booking's conversation, then accept the time they propose. To send a lesson request, see [Requests and waitlists](/en/guide/members/requests-and-waitlists). ## 1. Follow a coaching request [#1-follow-a-coaching-request] Open **My account**, select the **Reservations** tab and select the lesson in **My upcoming reservations**. While the club has not set a time, the booking shows **Awaiting your club** and its **Time** reads **To be arranged**. Use the **Conversation** card at the bottom of the page to share your availability with the club and the coach: 1. Type your message in the **Write a message…** box. 2. Select the send arrow, or press <kbd>Enter</kbd>. Press <kbd>Shift</kbd> + <kbd>Enter</kbd> to start a new line. <figure> <img src="/images/guide/members/coaching-request.png" alt="Cours particulier request for Yoga with Thomas with the status Awaiting your club, the time To be arranged, and a conversation where Alex Morgan asked for a lesson and is typing a reply, with the message box and the send arrow outlined in orange." width="1280" height="800" /> <figcaption> The example yoga lesson request of Alex Morgan, with a reply being typed. The controls to use are outlined in orange. </figcaption> </figure> ## 2. Accept a proposed time [#2-accept-a-proposed-time] When the club suggests a time, your reservations list shows **Proposed:** followed by that time. The booking shows **Proposal awaiting your acceptance** and a **Time proposed** card with the date, the **Total** and what happens next: * **Confirmed on acceptance.** The lesson is confirmed as soon as you accept. * **Payment due after acceptance.** The lesson is then **Booked — payment due** until you pay it. See [Pay a reservation](/en/guide/members/reservations-and-coaching#3-pay-a-reservation). A **Free** label means the club offers the session. Select **Accept proposed time** to book it. In **My upcoming reservations**, a booking with a proposal waiting shows **Review proposal**. <figure> <img src="/images/guide/members/coaching-proposal.png" alt="Cours particulier booking for Tennis with Julien with the status Proposal awaiting your acceptance, a Time proposed card marked Free for an upcoming day from 18:00 to 19:00, a total of 0 Stars, Confirmed on acceptance, an Accept proposed time button outlined in orange, and the Sport coach conversation with View profile." width="1280" height="800" /> <figcaption> A free first tennis lesson proposed to the example member Alex Morgan. </figcaption> </figure> If the time does not suit you, write to the club in the conversation. When the club moves a booking that already had a time, the card reads **New time proposed** and offers **Keep my current time**. A proposal that has passed shows **Proposal expired**: select **Ask for a new time** to write to the club. ## 3. Read your coach's profile [#3-read-your-coachs-profile] When a coach answers from their own account, their name appears at the top of the conversation with **View profile**. It opens their biography and qualifications, with the date each one expires. A qualification past its date shows **Expired**. Select **Back to conversation** to return. <figure> <img src="/images/guide/members/coach-profile.png" alt="Coach profile page for Sport coach with a biography and three Qualifications: First aid certificate (PSC1) from the French Red Cross, Pool lifeguard certificate (BNSSA) marked Expired, and Sports instructor professional card, each with its issue and expiry dates, and a Back to conversation button outlined in orange." width="1280" height="800" /> <figcaption> The profile of the example coach Sport coach, whose lifeguard certificate has expired. </figcaption> </figure> # Your membership Check your membership and choose a plan. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/membership-and-credits Your membership gives you access to your club. [Credits](/en/guide#credits) are the club's own currency for bookings: each club picks its name, for example Stars ⭐ at Ukab Clubs. Both live in the **Billing** tab of **My account**. ## 1. Check your membership [#1-check-your-membership] Open **My account**, select the **Billing** tab, then **Membership**. **My subscriptions** shows your [plan](/en/guide#plan) with its status and the date it ends. <figure> <img src="/images/guide/members/membership.png" alt="Membership page of Jamie Chen showing a Valid membership and Paid plan, Adhésion 51 IÉNA (2026) - Abonnement Membre 2026, priced €1,800 per year and valid for one year, with a Change payment method button outlined in orange." width="1280" height="800" /> <figcaption> The yearly membership of the example member Jamie Chen. The control to use is outlined in orange. </figcaption> </figure> | Label | What it means | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | | **Valid membership** | Your membership is active. **Valid until** gives its end date. | | **Expired membership** | Your membership has ended. **Expired on** gives the date. | | **Paid**, **Unpaid** | For a renewing plan: **Paid** while the current period runs, **Unpaid** once its end date has passed without a renewal. | Plans renew automatically at the end of each period. To pay with another card, select **Change payment method**. Your payment settings open on a secure Stripe page, where you update your card before coming back to your account. To stop your plan, contact your club: the payment page only changes your card. Your membership is extended only when a renewal payment succeeds. If your card is refused, update it with **Change payment method** before the end date, or your membership ends on that date. When your club recorded your membership itself, without an online payment, the page has no **Change payment method** button. Ask your club about renewing it. <figure> <img src="/images/guide/members/membership-club.png" alt="My subscriptions card of Alex Morgan with a Valid membership and Paid plan, Adhésion 51 IÉNA (2026) - Abonnement Membre 2026, priced €1,800 per year and valid until 31 December 2026, without a Change payment method button." width="1056" height="248" /> <figcaption> Alex Morgan's membership, recorded by Ukab Clubs. </figcaption> </figure> If your membership has expired while a subscription is still linked to your account, the page shows **Your membership needs attention**. Select **View my invoices** to find what is due, or contact your club before you subscribe again. > The member area has no button to switch plans. Ask your club: the staff updates your membership > for you. ## 2. Choose a membership [#2-choose-a-membership] Without a membership, the page shows **No membership yet** and the plans your club offers. 1. Under **Choose your membership**, select a plan. Each plan shows its price and period. 2. Select **Pay … with Stripe**. The button repeats the price of the selected plan. 3. Pay on the secure Stripe page. You then come back to your account. <figure> <img src="/images/guide/members/membership-plans.png" alt="Choose your membership list with three plans priced €1,800, €4,500 and €6,300 per year, the Abonnement Cercle plan selected and a Pay €4,500 / year with Stripe button outlined in orange." width="1056" height="396" /> <figcaption> The three example plans of Ukab Clubs, seen by Louis Boulanger. </figcaption> </figure> While the payment is checked, the page shows **Confirming your payment**: wait for it to update and do not pay again. **Payment confirmed** means your membership is recorded. ### If the payment does not go through [#if-the-payment-does-not-go-through] | Message | What to do | | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | **Checkout was cancelled. No Stripe membership has been confirmed. You can try again.** | You left the Stripe page without paying. Pick a plan and select **Pay … with Stripe** again. | | **Checkout expired. You can start a new payment attempt.** | The payment page was open too long. Select **Start new Checkout**. | | **Missing Checkout session. Open the original Stripe return link to verify this payment.** | The return link lost its payment reference. Open the original link from Stripe to check the payment. | | **Retry Checkout** on the button | The payment page could not open. Select the button to try again. | | **Online payment is currently unavailable. Please contact your club for assistance.** | The club does not take online payments yet. Contact it to join. | <figure> <img src="/images/guide/members/membership-cancelled.png" alt="My subscriptions with No membership yet, the message Checkout was cancelled. No Stripe membership has been confirmed. You can try again, the three plans and a Pay €1,800 / year with Stripe button outlined in orange." width="1056" height="432" /> <figcaption> Louis Boulanger back from the Stripe page without paying. </figcaption> </figure> To top up your credits and follow how you spend them, see [Your credits](/en/guide/members/credits). # Messages Write to your club, read its replies and start a new conversation. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/messages Your member area includes a private chat with your club's team. Use it for any question about the club, such as a locker, parking or an event. Questions about a specific lesson go in that booking's conversation: see [Lessons with a coach](/en/guide/members/lessons-with-a-coach). ## 1. Open your messages [#1-open-your-messages] In your member area, select the **Inbox** icon next to **Sign out**. The **Inbox** opens on **Messages**. The number on **Messages** shows how many conversations have a reply you have not read yet. Select the arrow next to **Inbox** to go back to your account. Each conversation shows its subject, the last message and when it was sent. Your own messages start with **You:**, and the club's start with the name of the person who wrote. A dot next to the subject marks a conversation with an unread reply. <figure> <img src="/images/guide/members/chats.png" alt="Messages tab of Alex Morgan's inbox listing two unread conversations, Locker for the season and Parking during the tournament, each with an Open button, the second one outlined in orange, and a New chat button." width="1280" height="800" /> <figcaption> The conversations of the example member Alex Morgan with Ukab Clubs. The control to use is outlined in orange. </figcaption> </figure> ## 2. Read and answer a conversation [#2-read-and-answer-a-conversation] Select **Open** on a conversation. The club's messages sit on the left with the sender's name, yours on the right, each with its date and time. Opening the conversation marks it as read. 1. Type your answer in the **Write a message…** box at the bottom. 2. Select the send arrow, or press <kbd>Enter</kbd>. Press <kbd>Shift</kbd> + <kbd>Enter</kbd> to start a new line without sending. <figure> <img src="/images/guide/members/chat-conversation.png" alt="Parking during the tournament conversation with a Club team message about the car park closing on Saturday, and a reply from Alex Morgan being typed in the message box, outlined in orange with its send arrow." width="1280" height="800" /> <figcaption> A reply being written in the example conversation Parking during the tournament. </figcaption> </figure> Select **Chats** above the conversation to go back to the list. ## 3. Start a new conversation [#3-start-a-new-conversation] 1. On **Messages**, select **New chat**. 2. Enter a **Subject** that sums up your question. 3. Write your **Message**. 4. Select **Send**. The new conversation opens, and the club's team sees it in their inbox. Select **Cancel** to close the form without sending. <figure> <img src="/images/guide/members/new-chat.png" alt="New chat form with the subject Guest towels and a message asking to borrow two extra towels on Saturday, with Cancel and Send buttons, Send outlined in orange." width="1072" height="316" /> <figcaption> An example question for the club, ready to send. </figcaption> </figure> > The club answers from its [back office](/en/guide#back-office), as described in [Answer > messages](/en/guide/staff/chats). Replies from a team account without a name are signed **Club > team**. Depending on your [notification settings](/en/guide/members/notifications), you also get an > alert or an email when a reply arrives. # Notifications Read your club's alerts and choose which ones you receive in the app or by email. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/notifications Your club sends you notifications about your bookings, your membership and your messages, for example a reminder the day before a reservation. You read them in your member area and choose, for each kind of alert, whether it also reaches you by email. ## 1. Open your notifications [#1-open-your-notifications] In your member area, select the **Inbox** icon next to **Sign out**, then select the **Notifications** tab. The number on the **Inbox** icon counts your unread notifications and conversations together. Each notification shows a title, a short text, the booking or item it concerns, and when it arrived. A link takes you to the right page, such as **View reservation**. Unread notifications have a dot and a bold title. <figure> <img src="/images/guide/members/notifications.png" alt="Notifications tab, outlined in orange, of Jamie Chen's inbox with unread notifications, showing two Upcoming reservation reminders for Le Restaurant and Le Rooftop, each with a View reservation link." width="1280" height="800" /> <figcaption> Reservation reminders of the example member Jamie Chen at Ukab Clubs. The control to use is outlined in orange. </figcaption> </figure> ## 2. Sort and mark notifications as read [#2-sort-and-mark-notifications-as-read] * Select **Unread notifications** to see only the new ones, and **All notifications** to see everything again. * Select the envelope icon on a notification to **Mark as read**, or to **Mark as unread** if you want to come back to it. * Select **Mark all as read** to clear every unread notification at once. A message lets you **Undo** it for a few seconds. Use **Previous** and **Next** at the bottom of the list when you have many notifications. Opening a conversation in [Messages](/en/guide/members/messages) also marks its message alerts as read. ## 3. Choose what you receive [#3-choose-what-you-receive] Select the gear icon next to **Mark all as read** to open **Notification settings**. Each line is a kind of alert with two switches: * **In app** shows the alert in your notifications, * **Email** sends it to your email address. Turn a switch on or off. As the dialog says, **Changes are saved automatically.** It shows **Preferences saved** once done. Close the dialog with the cross. <figure> <img src="/images/guide/members/notification-settings.png" alt="Top of the Notification settings dialog, with the note Changes are saved automatically and the first alerts, Reservation updates, New date proposals, Sponsored members joining and Guests joining, each with In app and Email switches turned on." width="448" height="321" /> <figcaption> The top of the notification settings of the example member Jamie Chen, all turned on. The table below lists every alert. </figcaption> </figure> | Alert | When it arrives | | ----------------------------- | ---------------------------------------------------------------------------------- | | **Reservation updates** | A booking changes status, such as confirmed or canceled, or a deposit is refunded. | | **New date proposals** | The club proposes a time for a lesson or a booking. | | **Sponsored members joining** | Someone you sponsored joins the club. | | **Guests joining** | A guest you invited joins or accepts your invitation. | | **Reservation reminders** | 24 hours before your reservation. | | **Waitlist updates** | A waiting list place becomes a reservation for you. | | **Membership renewal** | 7 days before your membership period ends. | | **Session cancellations** | The club cancels a session you registered for. | | **Reservation messages** | A new message arrives in a booking's conversation. | | **Chat messages** | The club replies in [Messages](/en/guide/members/messages). | | **Club messages** | The club writes to you, for example about a package at reception. | | **Low credit balance** | Your credit balance runs low. | The last line, **Club newsletter**, only has an **Email** switch. Turn it off to stop receiving your club's newsletter. ## 4. Unsubscribe from the newsletter [#4-unsubscribe-from-the-newsletter] You stop the club newsletter in either of two ways: * From your member area: turn off the **Email** switch of **Club newsletter** in **Notification settings**. * From a newsletter email: select **Se désabonner / Unsubscribe** at the bottom of the email, then **Unsubscribe** on the page that opens. The page confirms **You are unsubscribed**. Some email apps also show their own unsubscribe button, which works the same way. Both ways change the same setting: after you unsubscribe from an email, the **Club newsletter** switch is off in your notification settings. Turn it back on to receive the newsletter again. For the unsubscribe page and its messages, see [Accept an invitation you received by email](/en/guide/members/invitations-from-email). # Sign in, activate your account and reset your password Sign in to your member area, set your password the first time and recover access. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/passwords-and-activation You sign in to your member area with your email address and a password you choose. You set that password once, when your account is created, and you can replace it at any time from the sign-in page. ## 1. Sign in [#1-sign-in] Select **My account** in the top navigation of the [member app](/en/guide#member-app). When you are signed out, the **Welcome back** page opens. 1. In **Club**, choose your club. The page remembers the last club you signed in to. 2. Type your **Email** and your **Password**. Select the eye icon to show or hide the password. 3. Select **Sign in**. Your member area opens. <figure> <img src="/images/guide/members/sign-in.png" alt="Welcome back sign-in form with an empty Club field showing Choose a club, outlined in orange, the email alex@example.com, an empty Password field with a Forgot password? link, a Sign in button and the line Not a member yet? Apply to a club." width="400" height="425" /> <figcaption> The sign-in form with the example member's email, before choosing the club. The field to use is outlined in orange. </figcaption> </figure> **Sign in** becomes available once the club, the email and the password are filled in. Not a member yet? Select **Apply to a club** to find a club and apply: see [Find a club and apply](/en/guide/members/find-a-club-and-apply). When your session expires, or your club signs you out, the page shows **Your session has ended. Please sign in again.** Sign in again to continue. ## 2. Set your first password [#2-set-your-first-password] How you set your first password depends on how you joined the club. | How you joined | Where you choose your password | | -------------------------------------------- | ------------------------------------------------------------ | | You applied online and the club accepted you | On your application, once every step after approval is done. | | The club created your account | From the activation link in the email the club sent you. | ### After an accepted application [#after-an-accepted-application] When your [application](/en/guide#application) is complete, it shows **Choose your password**. Type your password in **New password** and again in **Confirm password**, then select **Save password and open my account**. Your member area opens straight away. To get back to your application, see [Complete and sign your application](/en/guide/members/sign-your-application). On the club's own application website, the button reads **Save password and sign in**. It opens the sign-in page of the member app for your club: sign in with your email and the password you just chose. ### From an activation email [#from-an-activation-email] When the club adds you itself, you receive an email titled "Activate your … account", with the club's name in place of the dots. Open its link within 7 days. 1. Type your password in **Password** and again in **Confirm password**. 2. Select **Activate my account**. 3. When the page shows that your account is activated, select **Back to sign in** and sign in with your email and new password. <figure> <img src="/images/guide/members/activate-account.png" alt="Activate your account card with a filled Password and Confirm password, the password rule, an Activate my account button outlined in orange and a Back to sign in link at the bottom." width="400" height="385" /> <figcaption> The activation form, filled in with an example password. </figcaption> </figure> If the page says **Incomplete activation link**, open the full link from the email again. If the link no longer works, select **Request a new link** to set your password with a reset link, or ask the club to send you a new activation email. > Use at least 12 characters, with a lowercase letter, an uppercase letter, a digit and a symbol. > The form tells you when the password is too weak or when the two passwords do not match. ## 3. Forgot your password [#3-forgot-your-password] 1. On the sign-in page, select **Forgot password?**. 2. Check the **Club** and type your **Email**. 3. Select **Send reset link**. <figure> <img src="/images/guide/members/forgot-password.png" alt="Forgot password form with Ukab Clubs in the Club field, an example email address, a Send reset link button outlined in orange and a Back to sign in link." width="400" height="349" /> <figcaption> Asking for a reset link for an example email address at Ukab Clubs. </figcaption> </figure> The page then confirms that you will receive a reset link if an account matches. For your security, it shows the same message whether or not the address has an account. The email, titled "Reset your Champagne password", has a link that works for 30 minutes. ## 4. Choose a new password [#4-choose-a-new-password] Open the link in the email. The **Choose a new password** page opens. 1. Type your password in **New password** and again in **Confirm password**. 2. Select **Save password**. 3. When the page confirms the change, select **Back to sign in** and sign in with your new password. <figure> <img src="/images/guide/members/reset-password.png" alt="Choose a new password form with a filled New password and Confirm password, the password rule, a Save password button outlined in orange and a Back to sign in link." width="400" height="385" /> <figcaption> The reset form, filled in with an example password. </figcaption> </figure> If the page says **Incomplete reset link**, open the full link from the email again, or select **Request a new link** to start over. If the link has expired, ask for a new one from **Forgot password?**. # Request a booking or join a waitlist Register for group sessions, ask your club for a lesson and wait for a place when a time is full. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/requests-and-waitlists Not every activity is booked the same way. Group classes have sessions you register for, lessons need the club to arrange a time with you, and popular times fill up. From the **Club schedule** of your member area, you register, send a request or join a waitlist. For an ordinary booking with a time you choose, see [Book a reservation](/en/guide/members/book-a-reservation). ## 1. Read the group sessions [#1-read-the-group-sessions] Open **My account**, select the **Reservations** tab and scroll down to **Club schedule**. With **All scheduled activities** selected in **Activity**, the schedule lists the week's sessions of every activity, day by day. Pick one activity in **Activity** to see only its sessions, and use the arrows or **This week** to move between weeks. <figure> <img src="/images/guide/members/group-sessions.png" alt="Club schedule with All scheduled activities selected, showing a week of Cours collectif classes marked Registration required, a daily 12:30 Open studio marked Open access, and a Saturday Concert — matinée marked Already booked." width="1056" height="782" /> <figcaption> A week at Ukab Clubs: Cours collectif classes, the daily Open studio and Alex Morgan's concert. </figcaption> </figure> Each session shows its time, its name and a label: | Label | What it means | | ------------------------- | ----------------------------------------------------------------- | | **Registration required** | You register to take part. Select the session to book your place. | | **Open access** | No registration: come at the time shown. | | **Already booked** | You have a place. Select the session to open your booking. | | **Request sent** | You asked for a place and the club has not answered yet. | Select an **Open access** session to see its date, time and place. Select **Add to my calendar** to download it to your calendar app. <figure> <img src="/images/guide/members/open-access-session.png" alt="Open studio window with the date and time from 12:30 to 13:15, Main studio, the label Open access and an Add to my calendar button outlined in orange." width="384" height="262" /> <figcaption> The example Open studio session, open to every member without registration. The control to use is outlined in orange. </figcaption> </figure> When the week has no session, the schedule says **No sessions this week.** Select **View upcoming sessions** to jump to the next week that has one. ## 2. Register for a group session [#2-register-for-a-group-session] Select a session marked **Registration required**. A form opens with the place, the date and time, and the number of places left. The **Total** shows the price. <figure> <img src="/images/guide/members/session-registration.png" alt="Cours collectif form showing Main studio, a date from 15:00 to 16:00 with 6 places left, a Free total and a Book reservation button outlined in orange." width="448" height="286" /> <figcaption> Registering for an example Cours collectif class with 6 places left. </figcaption> </figure> Select **Book reservation**. The form confirms with one of these messages: | Message | What happens next | | ----------------------------------------------------------------- | -------------------------------------------------- | | **Reservation confirmed.** | Your place is booked. | | **Reservation booked. Pay from your reservations to confirm it.** | Select **Pay reservation** to pay and confirm it. | | **Request sent. Awaiting club approval.** | The club checks your request before confirming it. | Select **View reservation**, or **Pay reservation** when a payment is due, to open the booking. Select **Close** to go back to the schedule. ## 3. Request a reservation [#3-request-a-reservation] For some activities, such as private lessons, the club arranges the time with you. Choose the activity in **Activity**: the schedule shows a **Request a reservation** button. 1. Select **Request a reservation**. 2. In **Resource**, pick the lesson or coach you want. The form says **Time to be arranged with your club.** 3. Check the **Total** and your **Available balance**. **Subject to club approval** reminds you that nothing is final until the club answers. 4. Select **Request reservation**. <figure> <img src="/images/guide/members/request-reservation.png" alt="Cours particulier request form with Pilates with Sarah selected among five lessons at 80 Stars, the note Time to be arranged with your club, a Total of 80 Stars, an Available balance of 540 Stars, Subject to club approval and a Request reservation button outlined in orange." width="448" height="524" /> <figcaption> A request for the example lesson Pilates with Sarah, paid in Stars. </figcaption> </figure> The form says **Request sent. Awaiting club approval.** Your request appears in **My upcoming reservations** with the status **Awaiting your club**. The club then proposes a time in the booking's conversation: see [Lessons with a coach](/en/guide/members/lessons-with-a-coach) to follow it and accept the time. ## 4. Join a waitlist [#4-join-a-waitlist] When a session or a time is full, the club may let you wait for a place. * A full session shows **full** next to its date. The form says **Waitlist only. No credits debited.** * A full time, for example at a restaurant, is crossed out among the times. Select it: the form says **This time is full. Join the waitlist to be booked if a place frees up.** When a linked activity still has room, a button such as **Available at …** lets you switch to it. <figure> <img src="/images/guide/members/session-waitlist.png" alt="Cours collectif form for a full class from 15:00 to 16:00, a Free total, the note Waitlist only. No credits debited, and a Join waitlist button outlined in orange." width="448" height="318" /> <figcaption> A full example Cours collectif class with its waitlist. </figcaption> </figure> <figure> <img src="/images/guide/members/join-waitlist.png" alt="Le Restaurant booking form for 8 people: 12:00 and 12:30 are free, 19:00 to 21:00 are crossed out, 19:00 is selected with the message This time is full. Join the waitlist to be booked if a place frees up, and a Join waitlist button outlined in orange." width="1280" height="800" /> <figcaption> The example Le Restaurant, full in the evening for a table of eight. </figcaption> </figure> Select **Join waitlist**. The form shows your place, such as **Waitlist · #1**, with a **Leave** button. The waiting place also appears in **My upcoming reservations** with its position. <figure> <img src="/images/guide/members/session-waitlist-place.png" alt="Cours collectif form for a full class from 09:00 to 10:00 in Main studio, a Free total, the note Waitlist only. No credits debited, the label Waitlist · #3 and a Leave button outlined in orange." width="448" height="318" /> <figcaption> Alex Morgan, third on the waitlist of tomorrow's full 9:00 class. </figcaption> </figure> <figure> <img src="/images/guide/members/waitlist-upcoming.png" alt="Line of My upcoming reservations for Cours collectif with the label Waitlist · #3, Main studio, the time from 09:00 to 10:00 and a Leave button outlined in orange." width="1008" height="116" /> <figcaption> The same waiting place in Alex Morgan's upcoming reservations. </figcaption> </figure> When a place frees up, Champagne books it for you, in the order people joined the waitlist. You receive a **Waitlist updates** notification: open the booking to check its status and any payment due. To stop waiting, select **Leave** next to your waitlist place, in the form or in **My upcoming reservations**. # Your reservations Open a booking to read its details, pay it, change it or cancel it. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/reservations-and-coaching Once you have booked, each reservation has its own page in your member area. From there you read what the booking costs, pay it, change it or cancel it. To make a new booking, see [Book a reservation](/en/guide/members/book-a-reservation). ## 1. Open a reservation [#1-open-a-reservation] Open **My account**, select the **Reservations** tab and select a booking in **My upcoming reservations**. You can also select the **Next reservation** card of the **Overview** tab. The reservation page shows the activity, its status and the room, place or coach you booked. The details card starts with the **Starts** and **Ends** times side by side, followed by the **Reservation price**. Select **My reservations** at the top to go back to the list. <figure> <img src="/images/guide/members/reservation-detail.png" alt="Suite d'hôtel reservation in the Courtyard suite with the status Booked — payment due, Edit and Cancel reservation buttons, a details card with the start and end dates side by side at the top, the price of 750 Stars, the Cancellation line Price non-refundable: the cancellation notice has passed, the warning Your balance is 540 Stars. You need 210 Stars more, and a Top up my credits button." width="1280" height="800" /> <figcaption> The example Suite d'hôtel booking of Alex Morgan, starting tomorrow and waiting for payment. </figcaption> </figure> Each booking shows one of these statuses: | Status | Meaning | | ------------------------------------- | ---------------------------------------------------------------- | | **Awaiting your club** | Your request is with the club, which will set or confirm a time. | | **Proposal awaiting your acceptance** | The club proposed a time. Accept it to confirm your lesson. | | **Booked — payment due** | Your place is held and still needs to be paid. | | **Confirmed** | Your booking is settled. | | **Completed** | The booking is over. | | **Canceled** | The booking was canceled. | | **Proposal expired** | The time the club proposed has passed without your answer. | The two proposal statuses concern lessons with a coach: see [Lessons with a coach](/en/guide/members/lessons-with-a-coach). When a booking has more than one person, the details card also shows the **Number of people**. For stays, tickets and the people coming with you, see [Tickets, stays and people on your booking](/en/guide/members/tickets-stays-and-people). ## 2. Read the money details [#2-read-the-money-details] Depending on the club and the booking, the details card shows more lines about money: | Line | What it means | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Security deposit** | An amount the club holds during your booking and normally gives back afterwards. | | **Total including deposit** | The price plus the security deposit: what you pay. | | **Deposit status** | **Due with reservation payment**, **Held until refund**, **Refunded**, or the part refunded and the part the club kept. | | **Message from the club** | The club's explanation when it settles your deposit, for example why it kept part of it. | | **Cancellation** | **Free until** followed by a date: cancel before it to get the price back. **Price non-refundable: the cancellation notice has passed** means that date is over. | If you chose extras such as drinks when booking, they are listed under **Product charges** at the top of the page, with the quantity and the amount of each. Products that only the club tracks, such as a minibar, are not listed there. A stay with a deposit and product charges is shown in [Follow your stay](/en/guide/members/tickets-stays-and-people#3-follow-your-stay). ## 3. Pay a reservation [#3-pay-a-reservation] When a booking is **Booked — payment due**, the details card offers to pay it with your [credits](/en/guide#credits). * If your balance covers the price, select **Pay** followed by the amount, for example **Pay 30 Stars ⭐**. The credits are taken from your balance right away. * If it does not, the card shows your balance and the amount missing, as in the Suite d'hôtel example above. Select **Top up my credits** to buy more, then come back to pay. See [Your credits](/en/guide/members/credits). <figure> <img src="/images/guide/members/reservation-pay.png" alt="Details card of a Place à la piscine booking with its start and end times at the top, a Reservation price of 30 Stars and a Pay 30 Stars button outlined in orange." width="1056" height="236" /> <figcaption> Alex Morgan's balance of 540 Stars covers this example pool booking at Transat #5. The control to use is outlined in orange. </figcaption> </figure> Once paid, the booking becomes **Confirmed**. A temporary account created by booking online pays by card instead: its booking shows **Pay … by card**, which opens a secure payment page. Coming back, a message says whether the payment went through. See [Book online as a visitor](/en/guide/members/book-online#4-pay-and-follow-your-booking). <figure> <img src="/images/guide/members/reservation-card-payment.png" alt="Details card of a Suite d'hôtel booking with its start and end dates at the top, a price of €960.00 for 2 people, a Total including deposit of €1,010.00, a Security deposit of €50.00, the Deposit status Due with reservation payment, the Cancellation line with the free cancellation deadline, and a Pay €1,010.00 by card button outlined in orange." width="1056" height="348" /> <figcaption> The Penthouse stay booked online by the visitor Marc Lefort, still free to cancel. </figcaption> </figure> ## 4. Change or cancel a booking [#4-change-or-cancel-a-booking] Until the booking starts, you can change or cancel it yourself. * **Edit** opens the booking form so you can pick another resource, date or time. Select **Save**. When the price stays the same, the change is saved right away. When it changes, a summary shows the **Current price**, the **New price** and the difference: select **Confirm change** to apply it. On a confirmed booking, the form reminds you that the difference is debited from or refunded to your credit balance. * **Cancel reservation** opens a confirmation. It tells you whether the price will be refunded and how many credits return to your balance. When the cancellation notice has passed, it warns that the price will not be refunded. <figure> <img src="/images/guide/members/cancel-reservation.png" alt="Cancel this reservation? dialog stating that the action cancels the reservation and that 30 Stars will be returned to the credit balance, with Keep reservation and Cancel reservation buttons, Cancel reservation outlined in orange." width="448" height="188" /> <figcaption> The cancellation confirmation for Alex Morgan's paid pool booking. </figcaption> </figure> Select **Keep reservation** to close the dialog without changes, or **Cancel reservation** to confirm. Some activities only allow cancellation; their page has no **Edit** button. Once the booking has started, only the club can change it. After a cancellation, the page shows **Reservation canceled** with the credits returned to your balance, or says that the price was not refunded when you canceled after the deadline. # Complete and sign your application Fill in your application, sign it and send it to the club. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/sign-your-application Your [application](/en/guide#application) is a form in several steps, set up by the club. You answer one step at a time, review your answers and send them. The club then accepts or rejects your application. Once accepted, you may still have a few steps to finish, such as paying your [plan](/en/guide#plan). To start an application, see [Find a club and apply](/en/guide/members/find-a-club-and-apply). ## 1. Fill in each step [#1-fill-in-each-step] The top of the form shows where you are, for example **Step 2 of 3**. Answer the step, then select **Continue**. Your answer is saved and the next step opens. Select **Back** to return to the previous step. To jump to a step you already answered, select **Steps** and pick it in the list. Steps you have not reached yet are greyed out. <figure> <img src="/images/guide/members/application-steps.png" alt="Application to join Ukab Clubs for Nina Demo at step 2 of 3, Verify your email address, with the Steps button outlined in orange and its menu open on Your name, Verify your email address, Introduce yourself and Review." width="1280" height="800" /> <figcaption> The application of the example applicant Nina Demo, with the Steps menu open. The control to use is outlined in orange. </figcaption> </figure> Some steps need a little more: * **Verify your email address**: type your email and select **Send code**. Type the code you receive in **Six-digit verification code**, then select **Continue**. **Resend code** sends a new one after a short wait, and **Change email** lets you fix the address. If you already have an account or an application with this address, a message offers a button to go there, such as **Go to my account** or **Finish my application**. Once verified, the step shows your **Verified email**: select **Edit email** to change it with a new code, then **Save email**. * **Sponsors**: ask members of the club for their sponsor codes and type them in **Sponsor code 1**, **Sponsor code 2** and so on. * Plan: pick the plan you want. Each plan shows its price. * Address, phone, identity and visit: some clubs ask for them in the application rather than after approval. They work as described in [Finish the steps after approval](/en/guide/members/after-your-application#2-finish-the-steps-after-approval). Once your identity is verified, your name can no longer be changed here, nor can your identity or visit steps once completed. ## 2. Sign a document [#2-sign-a-document] When the club asks you to sign a document, such as its rules, the step shows the document name and a **Download document** link so you can read it first. 1. Select **Continue to Yousign**. Yousign is the online signature service the club uses. 2. Sign in the Yousign window. Depending on the club, it opens inside the page under **Sign with Yousign** or in Yousign's own site. 3. Once signed, you return to your application automatically. If the signature window stays blank, select **Open Yousign in a new tab**. After signing there, come back and select **Check signature**. While Yousign confirms it, the page shows **Confirming your signature…**. If the signature page says **Signature link unavailable**, select **Back to application** and continue to Yousign again. > Once you start the signature, your earlier answers can no longer be changed. Check them before you > select **Continue to Yousign**. ## 3. Review and send [#3-review-and-send] On the last step, select **Review application**. The **Review your application** page lists every answer. Select **Edit** next to an answer to change it. When everything is right, select **Submit application**. Your application now shows **Awaiting a decision**. You have nothing more to do until the club answers. <figure> <img src="/images/guide/members/application-submitted.png" alt="Application of Lucas Demo to Ukab Clubs with the Awaiting a decision status, the heading Application submitted and the text The club will review your application. No action is needed." width="1152" height="304" /> <figcaption> The sent application of the example applicant Lucas Demo. </figcaption> </figure> To check on your application later, use **Find my application** as described in [Find a club and apply](/en/guide/members/find-a-club-and-apply#4-find-your-application-again). The club's side of this review is described in [Review applications](/en/guide/staff/applications). Once the club has decided, see [After the club's decision](/en/guide/members/after-your-application). # Tickets, stays and people on your booking Add a booking to your calendar, download its ticket, follow your stay and invite the people coming with you. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/tickets-stays-and-people A confirmed booking's page gives you what you need on the day: a calendar file, a ticket when your club issues them, and where you are in your stay. It is also where you invite the people coming with you, and where you answer the bookings other members invite you to. To open a booking, see [Open a reservation](/en/guide/members/reservations-and-coaching#1-open-a-reservation). ## 1. Add a confirmed booking to your calendar [#1-add-a-confirmed-booking-to-your-calendar] When a booking is **Confirmed**, select **Add to my calendar** at the top of its page. A calendar file downloads: open it to add the booking to your phone or computer calendar. The page confirms **Calendar file downloaded**. <figure> <img src="/images/guide/members/reservation-confirmed.png" alt="Confirmed Place à la piscine booking at Transat #4 with an Add to my calendar button outlined in orange, Edit and Cancel reservation buttons, a details card with the start and end times, a price of 30 Stars and 2 people, and the People on this reservation card below." width="1280" height="800" /> <figcaption> Alex Morgan's paid pool booking for two, in three days. The control to use is outlined in orange. </figcaption> </figure> ## 2. Download your ticket [#2-download-your-ticket] If your club issues tickets, the page also has a **Reservation ticket** card. Select **Download PDF ticket** to get it. The QR code stays the same when the booking changes: download the PDF again for the latest dates. Tickets are only available for confirmed bookings. <figure> <img src="/images/guide/members/reservation-ticket.png" alt="Reservation ticket card explaining that the QR code stays the same when the reservation changes, with a Download PDF ticket button outlined in orange." width="1056" height="144" /> <figcaption> The ticket card of Alex Morgan's confirmed Concert — matinée booking for two. </figcaption> </figure> ## 3. Follow your stay [#3-follow-your-stay] From check-in time to check-out time, a confirmed booking shows where you are in your stay instead of its status: **Check-in**, **Ongoing**, **Check-out**, then **Checked out** once the club has recorded your departure. The extras you chose, such as drinks, are listed under **Product charges** at the top of the page. When the club holds a security deposit, the details card shows its amount and its **Deposit status**. The other money lines are explained in [Read the money details](/en/guide/members/reservations-and-coaching#2-read-the-money-details). <figure> <img src="/images/guide/members/reservation-stay.png" alt="Suite d'hôtel stay in the Garden suite with the label Ongoing, Product charges listing Welcome champagne × 1 for 65 Stars, an Add to my calendar button, and a details card with the start and end dates, the Reservation price of 887 Stars, 2 people, a Total including deposit of 1,387 Stars, a Security deposit of 500 Stars, the Deposit status Held until refund and the Cancellation line Price non-refundable: the cancellation notice has passed." width="1280" height="800" /> <figcaption> Jamie Chen's stay in the Garden suite, in progress since yesterday, with its deposit paid. </figcaption> </figure> ## 4. Invite the people coming with you [#4-invite-the-people-coming-with-you] When a booking is for more than one person, its page has a **People on this reservation** card. It counts the places taken, for example **2 / 2 occupants**, and lists the people you invited with their answer: **Invited**, **Accepted** or **Declined**. **Account linked** means the person already has an account at the club. 1. Enter the **Email** of the person coming with you. 2. If your club asks guests to have an account, choose the **Access duration** of their temporary account: **12 hours**, **24 hours** or **48 hours**. 3. Select **Send invitation**. The person receives an email to accept or decline. Select **Remove** to take someone off the booking. When every place is taken, the form says so: remove someone to invite another person. An invitation that expired without an answer still holds its place until you remove it. <figure> <img src="/images/guide/members/reservation-people.png" alt="People on this reservation card showing 2 / 2 occupants, Jamie Chen invited with an Account linked label and a Remove button outlined in orange, and an empty Email field with the note Every place is taken: remove someone to invite another person." width="1056" height="356" /> <figcaption> Jamie Chen, invited to the example pool booking and not answered yet. Every place is taken, so another invitation needs a place freed first. </figcaption> </figure> ## 5. Bookings you are invited to [#5-bookings-you-are-invited-to] Another member can invite you to join their booking. Their invitations appear in **Invited reservations** at the bottom of the **Reservations** tab, with the place, the booking status, **Invited by** and the host's name, the date and your answer. <figure> <img src="/images/guide/members/invited-reservations.png" alt="Invited reservations card with Transat #4, Confirmed, Invited by Alex Morgan with the date and time of the booking, an Invited label, and Accept invitation and Decline buttons, Accept invitation outlined in orange." width="1056" height="196" /> <figcaption> Jamie Chen's invitation to Alex Morgan's pool booking. </figcaption> </figure> Select **Accept invitation** or **Decline**. Your answer then shows **Accepted** or **Declined**, and **Invitation unavailable** means the invitation has expired. Once you have accepted a confirmed booking that comes with a ticket, select **Download ticket** to get it. To answer from the invitation email instead, see [Accept an invitation you received by email](/en/guide/members/invitations-from-email). # Your member area Find your way around your member area, check in at the club and update your profile. Source: https://docs.champagne.dev-ukab.fr/en/guide/members/your-account Your member area gathers everything about your membership: your bookings, your billing, your invitations and your messages with the club. Sign in to the [member app](/en/guide#member-app) and select **My account** to open it. This page walks through what you see first and where each section leads. ## 1. Read the header [#1-read-the-header] The top of the page shows your name, your account status and your club. **Active** means you can use the club and book. Other statuses are explained in [When your account is limited](/en/guide/members/account-status). Three buttons sit on the right: * the **Inbox** icon opens your messages and notifications. A number on it counts the unread conversations and notifications, * **Help** opens the page of this guide that explains the screen you are on, * **Sign out** closes your session on this device. Below the header, four tabs lead to the sections of your member area: **Overview**, **Reservations**, **Billing** and **Invitations**. <figure> <img src="/images/guide/members/account-overview.png" alt="Overview tab of Alex Morgan's member area with his last entry and an I'm in the building button outlined in orange, a Membership card for Adhésion 51 IÉNA (2026) - Abonnement Membre 2026 valid until 31 December 2026, a Next reservation card for a Suite d'hôtel marked Booked — payment due, a Billing card reading 3 reservations to pay with 860 Stars to pay, and the My profile and My club cards." width="1280" height="800" /> <figcaption> The Overview tab of the example member Alex Morgan at Ukab Clubs. The control to use is outlined in orange. </figcaption> </figure> ## 2. Check in when you arrive [#2-check-in-when-you-arrive] The first card of the **Overview** tab records your visits. When you reach the club, select **I'm in the building**. The card then shows the time of your last entry, for example **Last entry today at** followed by the time, or **Last entry on** followed by the date of an earlier visit. Before your first visit it reads **No entry recorded yet**. ## 3. See your account at a glance [#3-see-your-account-at-a-glance] Three cards summarize your account. Select a card to open the matching page. | Card | What it shows | Opens | | -------------------- | ------------------------------------------------------------------------------ | ----------------------------- | | **Membership** | Your plan or **Valid membership**, with its end date, or **No membership yet** | Your membership page | | **Next reservation** | Your next booking with its date and status, or **Nothing booked** | That booking's detail page | | **Billing** | The invoices due, else the reservations to pay, or **All paid** | Your invoices or reservations | The **Billing** card counts the invoices due first. When you owe no invoice but some bookings are **Booked — payment due**, it shows **1 reservation to pay** or the number of reservations, with their total under it, for example **… Stars to pay**. Selecting the card then opens your reservations so you can pay them. Further down, **My club** shows your club's description and address. The **I'm in the building** card and the **Next reservation** card only appear while you can book: they are hidden when your account is suspended, banned or disabled. Guest accounts see **Next reservation** without the **Membership** and **Billing** cards. ## 4. Update your profile [#4-update-your-profile] The **My profile** card shows the details your club has on file. To change them: 1. Select **Edit**. 2. Update your first and last name, **Phone**, **Date of birth**, **Gender** or address. 3. Select **Save changes**, or **Cancel** to leave everything as it was. <figure> <img src="/images/guide/members/profile-editor.png" alt="My profile card in edit mode with first name Alex, last name Morgan, a phone number being typed, date of birth, gender and address fields, and Save changes and Cancel buttons, Save changes outlined in orange." width="696" height="656" /> <figcaption> Alex Morgan's profile with an example phone number being added. </figcaption> </figure> Once you start an address, its main fields become required. To remove an address, empty all its fields. ## 5. Find the other sections [#5-find-the-other-sections] Some tabs group several pages. Select a page name at the top of the tab to switch. | Tab or button | Pages | Guide | | ---------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **Reservations** | Your bookings and the club schedule | [Book a reservation](/en/guide/members/book-a-reservation), [Your reservations](/en/guide/members/reservations-and-coaching) | | **Billing** | **Membership**, **Credits**, **Invoices** | [Your membership](/en/guide/members/membership-and-credits), [Your credits](/en/guide/members/credits), [Billing](/en/guide/members/billing) | | **Invitations** | **Club guests**, **Sponsorship** | [Guests and sponsorship](/en/guide/members/guests-and-sponsorship) | | **Inbox** icon | **Messages**, **Notifications** | [Messages](/en/guide/members/messages), [Notifications](/en/guide/members/notifications) | > The **Reservations** and **Invitations** tabs appear while your account is active. Guest accounts > only see the pages that apply to their visit. Contact your club if a section you expect is > missing. When your club limits your account, or a page asks for a valid membership, see [When your account is limited](/en/guide/members/account-status). # Your account Change your name, photo, biography and password, check your qualifications, or sign out. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/account The **My account** page shows the staff profile you use in the [back office](/en/guide#back-office). From there you can change your name, photo and biography, change your password and sign out. ## 1. Open your account [#1-open-your-account] In the left menu, select **My account**. The page shows your name, your email address, your **Biography** and your **Qualifications** when you have some. <figure> <img src="/images/guide/staff/account.png" alt="My account page of the Sport coach account with its email address, a biography, three Qualifications (a first aid certificate, a pool lifeguard certificate marked Expired and a professional card), and Edit profile, View member profile, Change password and Sign out buttons." width="1280" height="800" /> <figcaption> The account of the example coach Sport coach, with two diplomas and a professional card. </figcaption> </figure> ## 2. Edit your profile [#2-edit-your-profile] Members see your name, photo and biography when you chat with them. Select **Edit profile** to change them. 1. Select the photo circle to add a photo, or to replace or remove the current one. You can then adjust the framing and select **Apply crop**. 2. Change your **Name** if needed. 3. Write a few words about yourself in **Biography**. 4. Open the **Preview** tab to see your profile as members see it. 5. Select **Save changes**. <figure> <img src="/images/guide/staff/edit-profile.png" alt="Edit profile window with Edit and Preview tabs, a photo circle, the name Sport coach and an example biography, with Cancel and Save changes buttons." width="1280" height="800" /> <figcaption> Adding an example biography to the Sport coach profile. The control to use is outlined in orange. </figcaption> </figure> To check your profile later without editing it, select **View member profile**. ### Qualifications [#qualifications] When your administrator records a diploma or a professional card in **Team**, it appears under **Qualifications** on your account and in the profile members see. Each one shows its type (**Diploma** or **Professional card**), its title, the issuing organization, and the **Issued on** and **Expires on** dates when known. An expired one shows **Expired**. Members do not see the document number or the file, and other kinds of documents do not appear. To add or update a qualification, ask your administrator. See [Manage your team](/en/guide/admins/manage-your-team). ## 3. Change your password [#3-change-your-password] Select **Change password**, enter your **Current password**, then your **New password** and repeat it in **Confirm password**. A password needs at least 12 characters with a lowercase letter, an uppercase letter, a digit and a symbol. Select **Save changes**. > After you change your password, Champagne signs you out. Sign in again with your new password. If you forgot your current password, sign out and select **Forgot password?** on the sign-in page. ## 4. Change language or sign out [#4-change-language-or-sign-out] The language and dark mode switches sit at the top right of every page. See [Sign in and find your way](/en/guide/staff/getting-started). To leave the back office, select **Sign out** at the bottom of your account card. # Review applications Read an application, then accept or reject it. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/applications When someone wants to join your club, they fill in your online [application](/en/guide#application) form. The **Applications** section lists these applications so your team can read each one and accept or reject it. To change the questions of the form itself, see [Application forms](/en/guide/admins/onboarding-forms). ## 1. See the applications [#1-see-the-applications] In the left menu, select **Applications**. The list shows each **Applicant**, the date the application was **Submitted** and its **Status**. <figure> <img src="/images/guide/staff/applications.png" alt="Applications section with a search field, Manage onboardings and Filters (1) buttons, and a list of applicants with their submission date and a Submitted or Accepted status." width="1280" height="800" /> <figcaption> Example applications to Ukab Clubs, waiting or already accepted. </figcaption> </figure> | Status | Meaning | | ---------------- | --------------------------------------------------------------------------------------------------- | | **Draft** | The applicant started the form and has not sent it yet. Its date shows **Not submitted**. | | **Submitted** | The applicant sent the form. It is waiting for a decision. | | **Under review** | Also waiting for a decision. The list shows it as **Submitted** and the filters count it with them. | | **Accepted** | Your team accepted the application. | | **Rejected** | Your team rejected the application. | A draft whose applicant has not given a name yet is titled **Application in progress**. An accepted application may also show **Steps**, such as **Steps 0/1**. These are the steps the new member still has to complete after approval, for example paying their [plan](/en/guide#plan). By default, the list shows **Submitted** and **Accepted** applications. Select **Filters** to choose other statuses or a submission date range, then select **Apply filters**. Type a name or email address in **Search by name or email…** to find one applicant. When nothing matches, select **Clear search and filters**. ## 2. Read an application [#2-read-an-application] Select an applicant in the list. The page shows their name and status, the **Onboarding revision** (the version of the form they filled in) and the submission date, then: * **Sponsors**, when your form asks for sponsor codes: the members whose code the applicant used, with a link to their profile, or **No sponsor code has been redeemed yet.** * **Rejection reason**, for a rejected application with a reason. * **Submitted information**: each question of your form with the applicant's answer. A question left empty shows **No answer** when it was optional and the application was sent, and **Not completed** otherwise. The chip next to the title counts the answered questions. Built-in questions carry their name in the onboarding editor, such as **Applicant name**, **Applicant address**, **Plan selection**, **Sponsorship** and **Verified email**. When your form asks for them before approval, the identity check result and the booked visit also appear here, and the identity actions below work the same way. * **After approval**: the steps the applicant completes once accepted, if your form has any. Each step shows **Waiting for approval** before the decision, **Not completed** after it, then the answer and its date, or **Payment confirmed** for a payment. An identity step also shows what Stripe checked: **Document**, **Selfie**, **Date of birth** with the age check, and **Name**. While the step is the applicant's current one, select **Allow a new attempt** to let them try again, for example after **Under the minimum age** or **The name does not match the application**, or **Mark as verified** to complete it yourself. Champagne first asks **Mark the identity as verified?**, since Stripe did not confirm every check. * **Status history**: when the application was started, submitted and decided. Select a change to see who made it, with the reason if any: a team member, **System** for automatic changes, **Applicant · identity not authenticated** for the applicant, or **Staff · identity not available** when no name is recorded for the team member who made the change. * **Email history**: each email sent to the applicant, such as the **Acceptance** or **Rejection** email or a **Finalization reminder**, with its state: **Sending**, **Accepted by provider**, **Failed** or **Uncertain**. **Accepted by provider** means the email left Champagne; it does not confirm it reached the inbox. * **Text message history**, when your form verifies the applicant's phone, before or after approval: each code texted to them, marked **Text sent**, **Text delivered** or **Text not delivered**. <figure> <img src="/images/guide/staff/application.png" alt="Application of Lucas Astel with a Submitted status, Accept and Reject buttons both outlined in orange, the Submitted information card listing three answers and the start of the Status history." width="1280" height="800" /> <figcaption> The application of the example applicant Lucas Astel. The controls to use are outlined in orange. </figcaption> </figure> <figure> <img src="/images/guide/staff/application-sponsors.png" alt="Application of Lucas Demo, Submitted, with Accept and Reject buttons, a Sponsors card listing Alex Morgan and his email address, and the Submitted information card with the answer Alex Morgan under Sponsorship." width="1280" height="800" /> <figcaption> The example applicant Lucas Demo, who used the sponsor code of Alex Morgan. </figcaption> </figure> ## 3. Accept or reject [#3-accept-or-reject] Select **Accept** or **Reject**. A confirmation window opens. Once an application is linked to a person in **People**, both buttons disappear and **Open person** takes their place at the top of the page. An application that is not linked yet shows only the decision it does not have: **Reject** on an accepted one, **Accept** on a rejected one. * **Accept**: select **Confirm acceptance**. Champagne creates the person's profile and sends them an email to continue their registration. A visit they booked while applying is moved to their profile. An **Open person** button then takes you to their profile in **People**. * **Reject**: you may type a **Reason (optional)**, then select **Confirm rejection**. The applicant receives an email that includes your reason. A visit they booked while applying is canceled. <figure> <img src="/images/guide/staff/reject-application.png" alt="Reject application? window with an example reason, Our membership is full for this season, and Cancel and Confirm rejection buttons." width="1280" height="800" /> <figcaption> Rejecting the example application of Lucas Astel with a short reason. </figcaption> </figure> > A rejected applicant who tries to apply again is asked to contact your club first. If you change > your mind, open the application and select **Accept**. # Blocked periods, sessions and waitlists Close a resource for a while, find the sessions of a group class and check the waitlist. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/blocked-periods-and-waitlists The buttons at the top right of the calendar in **Reservations** close a resource for a while, list the sessions of a group class and show who is waiting for a place. The buttons you see depend on the category and on your [role](/en/guide#role). To read the calendar, see [Handle reservations](/en/guide/staff/reservations). ## 1. Block a period [#1-block-a-period] A blocked period closes a resource for a while, for example during maintenance. Nobody can book it. **Block periods** opens **Manage blocked periods**. Choose what it **Applies to**: **All resources** or one of them. Enter the start and end, the **Reason (optional)**, then select **Block period**. The period appears in red in the calendar. You can also open the **Blocked period** tab at the top of the **Add reservation** form. Fill in the dates and an optional reason, then select **Block period**. This tab is only shown if your role lets you change the category. ## 2. Reopen a blocked period [#2-reopen-a-blocked-period] To reopen a blocked period, select its red **Blocked** block in the calendar. The window shows the dates and the reason. Select **Remove block** to make the period bookable again. <figure> <img src="/images/guide/staff/remove-blocked-period.png" alt="Remove blocked period window for an upcoming day from 09:00 to 11:00 with the reason Préparation et entretien de l'espace, and the buttons Cancel and Remove block." width="384" height="276" /> <figcaption> A blocked period of the example meeting room Salle de réunion. The control to use is outlined in orange. </figcaption> </figure> ## 3. Find the sessions of a group class [#3-find-the-sessions-of-a-group-class] **Manage sessions** replaces **Block periods** in group-class categories. It lists the **Dated** and **Recurring** sessions members can book, and **Add session** creates one. A warning icon next to the button means the category has no sessions yet. Your administrator usually sets the sessions up. See [Sessions, guest lists and blocked periods](/en/guide/admins/sessions-and-blocked-periods). ## 4. Check the waitlist [#4-check-the-waitlist] **Waitlist** appears in categories where members pick a time and a number of people. It lists who is waiting for a place in the next 30 days, with the date and time they want, the number of people and their position in the queue. When nobody is waiting, it shows **Nobody is waiting in the next 30 days.** <figure> <img src="/images/guide/staff/reservations-waitlist.png" alt="Waitlist window listing Gabriel Morel for 2 people in position #1 and Arthur Perrin for 4 people in position #2, both for the same evening at 20:00." width="448" height="250" /> <figcaption> Two parties waiting for a table at the example restaurant Le Rooftop. </figcaption> </figure> When a place frees up, for example after a cancellation, Champagne books it within a minute for the people waiting, in queue order, with any payment due. The place does not stay free for someone at the desk. For group classes, the waitlist of each session appears in its panel in **Club calendar**. See [Use the club calendar](/en/guide/staff/planning#2-follow-a-session-and-mark-attendance). # Answer messages Read and answer member messages from the Chats inbox, and start a new conversation. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/chats The **Chats** section is your club's inbox. Members write to the club from their member account, and your team answers here. This page shows how to spot new messages, reply, start a conversation and tidy the inbox. ## 1. Open the inbox [#1-open-the-inbox] In the left menu, select **Chats**. Each line is a conversation with one member: its subject, the member's name, the last message and when it arrived. Messages from today show the time, older ones show the date. <figure> <img src="/images/guide/staff/chats.png" alt="Chats inbox with two conversations with Alex Morgan: Locker for the season, shown in bold with a dot because it is unread, and Parking during the tournament." width="1280" height="800" /> <figcaption> The inbox of the example club Ukab Clubs, with one unread conversation from Alex Morgan. </figcaption> </figure> A conversation with a new message from the member stands out: * a dot on the member's initials, * the subject in bold and the last message in darker text, * the time in color. A dot on **Chats** in the left menu tells you that at least one conversation is unread, wherever you are in the [back office](/en/guide#back-office). ## 2. Reply to a member [#2-reply-to-a-member] Select a conversation to open it. The member's messages sit on the left, your team's replies on the right, each with the sender's name and the date. Opening the conversation marks it as read. 1. Type your answer in the **Write a message…** box at the bottom. 2. Select **Send**, or press <kbd>Enter</kbd>. Press <kbd>Shift</kbd> + <kbd>Enter</kbd> to start a new line without sending. <figure> <img src="/images/guide/staff/chat-conversation.png" alt="Parking during the tournament conversation with Alex Morgan, showing a Club team message and a reply being typed next to the Send button." width="1280" height="800" /> <figcaption> A reply being written in the example conversation Parking during the tournament. The control to use is outlined in orange. </figcaption> </figure> Your replies are signed with the name of your staff account. Messages sent by an account without a name are signed **Club team**. Select **Open person**, shown if you can view member profiles, to open the member's page in **People**, and **Chats** to go back to the inbox. ## 3. Start a new conversation [#3-start-a-new-conversation] To write to someone first, select **New chat** in the inbox. 1. In **Person**, type a few letters of their name in **Search a person** and pick them in the list, which holds the same people as the **People** section. 2. Enter a **Subject**, such as the topic of your message. 3. Write your **Message**. 4. Select **Send**. The new conversation opens and appears in the inbox. <figure> <img src="/images/guide/staff/new-chat.png" alt="New chat dialog with Alex Morgan chosen as Person, the subject Court lights on Friday and a short message, with Cancel and Send buttons." width="1280" height="800" /> <figcaption> A new example conversation for Alex Morgan, ready to send. </figcaption> </figure> ## 4. Archive finished conversations [#4-archive-finished-conversations] Select the box icon at the end of a line to **Archive** a conversation you no longer need in the inbox. Switch between **Active** and **Archived** at the top of the list to see each group, and select the same icon in **Archived** to **Restore** a conversation. An empty inbox says **No active conversations yet.** or **No archived conversations.** If it says **Conversations unavailable**, the inbox could not load: reload the page, and tell your administrator if the message stays. ## 5. Messages about a reservation [#5-messages-about-a-reservation] Team members with the **Staff** [role](/en/guide#role), such as coaches, can also receive messages about bookings. For them, the inbox has two tabs: **Customers** for the conversations above and **Reservations** for messages linked to a booking. Each reservation line shows the customer's name, the activity, the resource, the session date, or **Time to be arranged** when no time is set yet, and the booking status: | Status | What it means | | ------------------------ | -------------------------------------------------- | | **Pending** | The booking waits for the club's answer. | | **Booked · payment due** | The time is set and the customer still has to pay. | | **Confirmed** | The booking is confirmed. | | **Completed** | The booking is marked as completed. | | **Canceled** | The booking was canceled. | <figure> <img src="/images/guide/staff/chats-reservations.png" alt="Reservations tab of Chats for a coach, outlined in orange, listing three private lesson conversations with Alex Morgan, Sam Rivera and Jamie Chen, with their session date or Time to be arranged, and their booking status." width="1280" height="800" /> <figcaption> The reservation conversations of the example coach account Sport coach. </figcaption> </figure> Selecting a reservation conversation opens the booking in **Club calendar**, where you read and answer the messages next to the session details. > The tabs you see depend on your permissions. **Customers** needs the **Chat with customers** > permission, and **Reservations** needs **Manage reservation messages**. Ask your administrator if > you should answer member conversations or reservation messages and cannot see them. # Check guests in and out Validate tickets at the door, record departures and refund security deposits. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/check-in-and-deposits This page covers the moments when a guest arrives and leaves. You can check a member's ticket at the door and mark them present, follow a stay while it is in progress, record the departure with **Check out** and give back the security deposit. A security deposit is an amount the guest pays on top of the price and gets back after the stay, for example for a hotel suite. ## 1. Check a ticket at the door [#1-check-a-ticket-at-the-door] When a [reservation category](/en/guide#reservation-category) gives members a QR code ticket, members download it from their reservation. The ticket shows a QR code and a six-digit code. Your administrator turns this on with the **QR code ticket** option of the category. In **Reservations** or **Club calendar**, select the category, then select **Tickets** above the calendar. The button appears for categories with tickets, if your [role](/en/guide#role) includes the **Validate attendance and tickets** permission. <figure> <img src="/images/guide/staff/ticket-code.png" alt="Tickets window with the Scan and Code choices, Code selected, the code 510004 typed in the field and a Verify ticket button." width="384" height="276" /> <figcaption> Typing the example ticket code 510004 by hand. The control to use is outlined in orange. </figcaption> </figure> 1. With **Scan**, hold the ticket's QR code in front of your device's camera. The ticket is read as soon as the code is in view. 2. With **Code**, type the six digits printed under the QR code, then select **Verify ticket**. Use it when the camera cannot read the code. 3. Read the result. * **Valid ticket** shows the guest's name and photo, the category, the **Date**, the **Resource**, the number of **Guests** and the attendance already recorded. People the guest invited are listed under **Invited guests**, marked **Accepted** or **Invited**. * **Invalid ticket** means the code is unknown, or the booking is canceled, not confirmed yet, or already over. Tickets of a category without tickets are refused too. <figure> <img src="/images/guide/staff/ticket-valid.png" alt="Tickets window showing Valid ticket for Alex Morgan in the Événement category, with the date, the Event space resource, 2 guests, an Unmarked attendance, and the Verify next and Mark present buttons." width="384" height="414" /> <figcaption> The ticket of the example member Alex Morgan for a concert matinee. </figcaption> </figure> 4. Select **Mark present** to record the guest's arrival. The button then reads **Present**. 5. Select **Scan next** or **Verify next** for the following guest. > The browser must be allowed to use the camera. If it is not, a message says the camera is > unavailable and offers to try again. Switch to **Code** and type the code instead. After many > attempts in a short time, the check pauses for one minute. You can also mark attendance by hand on the reservation page or in a session's panel. See [Handle reservations](/en/guide/staff/reservations) and [Use the club calendar](/en/guide/staff/planning). ## 2. Follow a stay and check a guest out [#2-follow-a-stay-and-check-a-guest-out] A confirmed reservation shows where the stay stands in a label next to the guest's name. The times come from the reservation, and from the check-in and check-out times of the resource when your administrator sets them. | Label | What it means | | --------------- | --------------------------------------------------------------------------------- | | **Check-in** | The guest can arrive: the check-in time has passed. | | **Ongoing** | The reservation has started and is not over yet. | | **Check-out** | The reservation has ended and the guest can still leave until the check-out time. | | **Checked out** | Someone recorded the guest's departure. | <figure> <img src="/images/guide/staff/check-out.png" alt="Reservation page of Jamie Chen in the Garden suite for 2 people, marked Ongoing, with a security deposit of 500 Stars held until refund, the buttons Check out, Edit and Cancel, and the product charges of the stay." width="1280" height="800" /> <figcaption> The example member Jamie Chen during a stay in the Garden suite. </figcaption> </figure> While the stay is in progress, select **Check out** at the top of the reservation page when the guest leaves. **Check out** and **Undo check-out** need the **Validate attendance and tickets** permission. The departure time appears in a **Checked out** card next to the dates. * Without a deposit to give back, the reservation becomes **Completed**. * With a paid deposit, it stays **Confirmed** until the deposit is refunded. If you checked out the wrong guest, select **Undo check-out**. The reservation goes back to **Confirmed**. Once the deposit is refunded, the check-out can no longer be undone. ## 3. Refund the security deposit [#3-refund-the-security-deposit] The line under the guest's name shows the deposit and where it stands. | Deposit state | What it means | | -------------------------------- | ---------------------------------------------------- | | **Due with reservation payment** | The guest has not paid the reservation yet. | | **Held until refund** | The club holds the deposit. | | **Refunded** | The whole deposit went back to the guest. | | An amount refunded and kept | Part of the deposit went back and the rest was kept. | **Refund deposit** appears once the guest has left, either after **Check out** or once the reservation's check-out time has passed, if your role includes the **Create and change reservations** permission. <figure> <img src="/images/guide/staff/checked-out.png" alt="Reservation page of Sam Rivera in the Terrace suite marked Checked out, with an Undo check-out button, the line Security deposit: 500 Stars, Held until refund, a Refund deposit button outlined in orange and a Checked out card next to the start and end dates." width="1280" height="800" /> <figcaption> The example member Sam Rivera after leaving the Terrace suite. </figcaption> </figure> 1. Select **Refund deposit**. 2. In **Amount to refund**, keep the full amount or lower it. The amount kept appears under the field. 3. When you keep part of the deposit, explain why in **Message to the customer**. 4. Select **Refund in full**, or **Refund** followed by the amount. <figure> <img src="/images/guide/staff/refund-deposit.png" alt="Refund deposit window with 400 in Amount to refund, 100 Stars kept, a message about a broken lamp and a Refund 400 Stars button." width="448" height="392" /> <figcaption> Refunding 400 of the example 500 Stars deposit and keeping 100. </figcaption> </figure> The money goes back the way the guest paid: to their card, or to their credit balance. The reservation becomes **Completed** and the guest is notified. Your message appears under the deposit line, and on the member's reservation under **Message from the club**. > Deposits that nobody refunds are returned in full after a delay set by your administrator in > **Settings**, under **Security deposits**. # Your first day The five things to learn first when you join your club's team, in order. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/first-day New to the [back office](/en/guide#back-office)? Learn these five tasks in this order. Each one links to the page that explains it step by step. What you see depends on your [staff role](/en/guide#role), so skip a task if its section is missing from your left menu. [Who sees what](/en/guide/admins/who-sees-what) lists what each permission shows. ### Sign in and find your way [#sign-in-and-find-your-way]Accept your invitation, sign in and learn the left menu. See [Sign in and find your way](/en/guide/staff/getting-started). ### Find a member [#find-a-member]Search the **People** list and open a member's profile. See [Manage members](/en/guide/staff/people). ### Handle the day's reservations [#handle-the-days-reservations]Read the calendar of a category, add a booking, move it or cancel it. See [Handle reservations](/en/guide/staff/reservations). ### Welcome people at the door [#welcome-people-at-the-door]Check tickets, record departures and refund security deposits. See [Check guests in and out](/en/guide/staff/check-in-and-deposits). ### Answer messages [#answer-messages]Read and answer member messages in **Chats**. See [Answer messages](/en/guide/staff/chats). Once you know these, the rest of this part of the guide covers [applications](/en/guide#application), member profiles, finance, newsletters and housekeeping. # Sign in and find your way Open the back office, switch between clubs and reach each section from the left menu. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/getting-started The [back office](/en/guide#back-office) is the website your club team uses every day. This page shows how to sign in, how the screen is organized and how to change club, language or display. ## 1. Sign in [#1-sign-in] Open the back-office address your club gave you. Enter the **Email** and **Password** of your staff account, then select **Sign in**. <figure> <img src="/images/guide/staff/sign-in.png" alt="Sign in to Champagne page with Email and Password fields, a Sign in button and a Forgot password? link." width="1280" height="800" /> <figcaption> The sign-in page, filled with an example address. The control to use is outlined in orange. </figcaption> </figure> > Your administrator sends you an invitation email. Open the link in that email to choose your name > and password. See [Invite your team](/en/guide/admins/invite-your-team). Forgot your password? Select **Forgot password?**, enter your email and follow the link you receive to choose a new one. <figure> <img src="/images/guide/staff/forgot-password.png" alt="Forgot password page with an Email field and a Send reset link button." width="448" height="244" /> <figcaption> The page that sends you a reset link. </figcaption> </figure> <figure> <img src="/images/guide/staff/reset-password.png" alt="Choose a new password page with New password and Confirm password fields and a Save password button." width="448" height="356" /> <figcaption> The page the link in the email opens. </figcaption> </figure> ## 2. Get to know the screen [#2-get-to-know-the-screen] After signing in, Champagne opens the first section you are allowed to use. <figure> <img src="/images/guide/staff/back-office.png" alt="Back office showing the left menu with People selected, the club name Ukab Clubs at the top and a list of members." width="1280" height="800" /> <figcaption> The People section of the example club Ukab Clubs. The numbers match the list below. </figcaption> </figure> 1. The left menu lists the sections you can open, such as **People**, **Reservations**, **Chats** or **Finance**. **My account** sits at the bottom, under **Team** for administrators. 2. The club name at the top left shows which club you are working on, when your account works for several clubs. 3. The section name next to it reminds you where you are. 4. **EN** or **FR** at the top right shows the language of the interface. Select it to switch to the other language. 5. The switch next to it turns dark mode on or off. **Help**, before the language, opens the page of this guide that explains the screen you are on. On a phone, it sits at the bottom of the menu. You only see the sections your [role](/en/guide#role) allows. A coach may see **Club calendar**, **My reservations** and **My availability**, while a manager also sees **People** or **Finance**. Ask your administrator if a section you need is missing: [Who sees what](/en/guide/admins/who-sees-what) lists what each permission shows. ## 3. Switch club [#3-switch-club] If you work for several clubs, select the club name at the top left and pick another club in the list. A check mark shows the club you are working on. Every section then shows that club's members, reservations and settings. <figure> <img src="/images/guide/staff/club-switcher.png" alt="Club menu open under Ukab Clubs, listing Astel, Ukab Clubs with a check mark, and Vosges." width="1280" height="800" /> <figcaption> The club menu of an account that works for three example clubs. </figcaption> </figure> ## Next steps [#next-steps] [Manage members](https://docs.champagne.dev-ukab.fr/en/guide/staff/people) [Handle reservations](https://docs.champagne.dev-ukab.fr/en/guide/staff/reservations) [Answer messages](https://docs.champagne.dev-ukab.fr/en/guide/staff/chats) # Housekeeping Follow and complete cleaning and preparation tasks. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/housekeeping **Housekeeping** helps the team keep rooms ready between stays. For each room you can tick the cleaning checklist, report its state, charge what the guest used and look back at what was logged. The section appears when your [role](/en/guide#role) includes housekeeping and one of your club's [reservation categories](/en/guide#reservation-category), such as hotel suites, uses housekeeping. ## 1. See which rooms need attention [#1-see-which-rooms-need-attention] Open **Housekeeping** in the left menu. Rooms are grouped by reservation category. Each card shows the room's last reported state and how many checklist items are still to do. <figure> <img src="/images/guide/staff/housekeeping-board.png" alt="Housekeeping page with the Suite d'hôtel category and four room cards, Courtyard suite, Garden suite, Penthouse and Terrace suite, each marked State not reported with 6 items left, Garden suite outlined in orange." width="1280" height="800" /> <figcaption> The four example hotel suites of the Suite d'hôtel category. The control to use is outlined in orange. </figcaption> </figure> **State not reported** means nobody has logged the room's state yet. Select a card to open the room's panel. ## 2. Tick the checklist [#2-tick-the-checklist] The **Checklist** tab groups tasks under **Every day** and **After each reservation**. The counter next to each group shows how many tasks are done. Tick a task when it is done: it is saved right away and crossed out. The information icon next to a ticked task shows who ticked it and when. <figure> <img src="/images/guide/staff/housekeeping-checklist.png" alt="Garden suite housekeeping panel on the Checklist tab, with three Every day tasks, such as Air the room, and three After each reservation tasks, such as Change the sheets, none ticked." width="1280" height="800" /> <figcaption> The checklist of the example room Garden suite. </figcaption> </figure> > Staff who configure reservation resources edit the checklist from the room's settings. See [Set up > the checklist](#5-set-up-the-checklist). ## 3. Report the room's state [#3-report-the-rooms-state] Open the **State** tab and select the state that matches the room: **Clean**, **Needs cleaning**, **Needs maintenance** or **Out of service**. Your choice is saved as soon as you select it, and the room's card shows it. <figure> <img src="/images/guide/staff/housekeeping-state.png" alt="State tab of the Garden suite panel with the four states Clean, Needs cleaning, Needs maintenance and Out of service, and an empty Note field." width="1280" height="800" /> <figcaption> The state choices for the example room Garden suite. </figcaption> </figure> Add details in **Note**, for example a broken lamp. The note is saved with the selected state when you click outside the field. On a room whose state was never reported, no state is selected yet: the note is saved when you select one. ## 4. Charge consumption and check the history [#4-charge-consumption-and-check-the-history] **Consumption** charges the products a guest used, such as minibar drinks, to their stay. 1. In **Reservation to charge**, pick the guest's stay. 2. Enter the quantity of each product consumed since the last report. 3. Check the amount in [credits](/en/guide#credits) shown above the button, then select **Report and charge consumption**. Only confirmed or completed stays of a member, already started and not paid by card, can be charged. The amount is taken from the member's credits, so a balance too low refuses the charge. Without one, the tab shows **No eligible stay to charge**. **History** lists every state, note and consumption logged for the room. ## 5. Set up the checklist [#5-set-up-the-checklist] The checklist of each room is edited in the **Reservations** section, if your role lets you configure reservation resources. 1. Open **Reservations** and select the category, such as **Suite d'hôtel**. 2. Select the room's name at the start of its row in the calendar. You can also open it from **Category settings**, in the resources list. The room's panel opens with the same tabs as in **Housekeeping**, plus **Resource** and **Products**. 3. Open the **Checklist** tab and select **Edit checklist**. 4. Change a task by editing its text. Select **Add task** under **Every day** or **After each reservation** to add one, and the cross next to a task to remove it. A window asks you to confirm with **Remove item**. 5. Select **Save checklist**. Empty tasks are left out, and the new list appears in **Housekeeping** for everyone. <figure> <img src="/images/guide/staff/housekeeping-edit-checklist.png" alt="Garden suite panel on the Checklist tab in edit mode, with three Every day tasks and three After each reservation tasks in editable fields, each with a remove cross, Add task buttons, and Cancel and Save checklist buttons." width="1280" height="800" /> <figcaption> Editing the checklist of the example room Garden suite. </figcaption> </figure> **Cancel** leaves the checklist as it was. Removed tasks disappear only when you save. # Manage a member's plans and credits Grant or remove a member's plan and read their credit history. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/member-plans-and-credits The **Memberships** and **Credits** cards, in the **Membership** tab of a profile, show what a member pays for and what they spend. This page shows how to grant a [plan](/en/guide#plan) by hand, remove one and read every credit movement. To open a profile, see [Work on a member's profile](/en/guide/staff/member-profile). ## 1. Grant or remove a plan [#1-grant-or-remove-a-plan] The **Memberships** card lists the person's plans. Each plan shows **Valid membership** or **Expired membership**, whether it is a **Subscription** or an **Add-on**, whether it is **Paid** or **Unpaid** when the payment is known, and its end date, or **No end date**. ### Grant a plan [#grant-a-plan] Use this to give a plan by hand, for example a free season pass. 1. Select **Manage plans**. 2. Choose the **Plan**. 3. Optionally, set an **Expiration date (optional)**. Leave it empty for a plan with no end date. 4. Optionally, add a **Reason (optional)** for the history. 5. Select **Grant plan**. <figure> <img src="/images/guide/staff/grant-plan.png" alt="Manage plans window with the Adhésion 51 IÉNA subscription chosen, an expiration date of 30 June 2027, an example reason and Close and Grant plan buttons, Grant plan outlined." width="448" height="408" /> <figcaption> Granting an example season pass to Alex Morgan until the end of June 2027. The control to use is outlined in orange. </figcaption> </figure> A granted plan does not create an invoice. If the person already has the same plan granted by hand, Champagne updates its expiration date. ### Remove a plan [#remove-a-plan] Select **Remove membership** on a plan, add a **Reason (optional)** and confirm with **Remove membership**. A plan granted by hand disappears immediately. A plan paid through Stripe may come back when Stripe sends its next update: to stop a paid subscription, cancel it in your payment account. Open **Technical details** on a plan to see its **Stripe price** and its **Membership ID**, for example when your Champagne provider asks for them. Each plan added, renewed or removed is kept in the **Membership history** of the profile. See [Read the history and signed documents](/en/guide/staff/member-profile#4-read-the-history-and-signed-documents). ## 2. Read the credit history [#2-read-the-credit-history] Select **View history** in the **Credits** card to open **Credit history**. Each line shows what the [credits](/en/guide#credits) were used for, such as a **Top-up**, a **Reservation**, a **Security deposit**, a **Refund**, a **Deposit returned** or an **Add-on**, with its date and the number of credits added or spent. For a top-up, the amount the member paid appears under the credits. <figure> <img src="/images/guide/staff/member-credit-history.png" alt="Credit history window for Alex Morgan listing two lounger reservations at -30 Stars, a Pilates with Sarah reservation at -80 Stars and its refund at +80 Stars, and a Weekend top-up of +600 Stars paid €50.00." width="512" height="446" /> <figcaption> The credit history of the example member Alex Morgan. </figcaption> </figure> # Work on a member's profile Find your way around a member's profile and read their attendance, activity and history. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/member-profile A member's profile gathers everything your club knows about them: details, [plans](/en/guide#plan), [credits](/en/guide#credits), status, tags, attendance, account activity and history. This page explains how the profile is organized and how to read the person's activity. To find a person first, see [Manage members](/en/guide/staff/people). The tasks you carry out from the profile have their own pages: * [Change a member's status](/en/guide/staff/member-status): suspend a member, block their bookings, ban or disable them, and give access back. * [Manage a member's plans and credits](/en/guide/staff/member-plans-and-credits): grant or remove a plan and read the credit history. * [Tags, notes and notifications](/en/guide/staff/member-tags-and-notifications): label a member, keep an internal note and send a notification. * [Keep a customer sheet](/en/guide/staff/reservation-people#4-keep-a-customer-sheet): record preferences and contraindications for categories such as beauty treatments. ## 1. Find your way around the profile [#1-find-your-way-around-the-profile] Select a person in **People** to open their profile. The top of the page shows their name, status, tags and email, with shortcuts to **Open application**, start a **New chat** and **Send notification**. <figure> <img src="/images/guide/staff/person-profile.png" alt="Profile of Alex Morgan with an Active status, the Founding member and Tournament players tags, Open application, New chat and Send notification buttons, the Profile, Membership, Activity and History tabs, and the Profile tab showing cards for Personal details, Legal status, Member status and Tags." width="1280" height="800" /> <figcaption> The profile of the example member Alex Morgan. The numbers match the tabs in the table below. </figcaption> </figure> The profile is organized in four tabs, each holding cards: | # | Tab | Card | What you can do | | - | -------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | **Profile** | **Personal details** | See name, phone, date of birth, gender and address. Select **Edit information** to change them. | | | | **Legal status** | Select **Edit legal status** to mark the person as an **Individual** or a **Company**. A company has a **SIRET** of exactly 14 digits and an optional **TVA number**. | | | | **Additional information** | Answer your club's extra questions. Select **Edit attributes**. The card appears only when your club has extra questions. | | | | **Member status** | Whether the person has normal access. See [Change a member's status](/en/guide/staff/member-status). | | | | **Tags** | The labels your team put on the person. See [Tags, notes and notifications](/en/guide/staff/member-tags-and-notifications). | | | | **Sponsorship** | Who sponsored the person, and whom they sponsored. | | | | **Invited guests** | The guests the member invited, when there are any. | | | | **Note** | An internal note about the person. Select **Edit note** to change it. | | 2 | **Membership** | **Memberships** | See the person's plans, grant a plan or remove one. See [Manage a member's plans and credits](/en/guide/staff/member-plans-and-credits). | | | | **Signed documents** | The documents the person signed during their application. See [step 4](#4-read-the-history-and-signed-documents). | | | | **Credits** | See the credit balance. Select **View history** for every top-up, reservation, deposit, refund and add-on. | | 3 | **Activity** | **Attendance** | How often the person was present, late or absent. See [step 2](#2-check-attendance). | | | | **Customer sheet** | Shown for categories that keep customer sheets. See [Keep a customer sheet](/en/guide/staff/reservation-people#4-keep-a-customer-sheet). | | | | **Building entries** | The last times the person told the club they arrived. See [step 3](#3-follow-the-account-activity). | | | | **Account activity** | The last sign-in and whether the account is activated. | | 4 | **History** | **Membership history**, **Status history** and **Profile history** | Every plan, status and profile change. See [step 4](#4-read-the-history-and-signed-documents). | Each tab has its own address, so a link you share opens the same tab. > The cards and buttons you see depend on your permissions. For example, **Member status** appears > only if you may change a member's status, and **Manage plans** only if you may manage memberships. > See [Who sees what](/en/guide/admins/who-sees-what). ## 2. Check attendance [#2-check-attendance] In the **Activity** tab, the **Attendance** card counts the person's reservations marked **Present**, **Late** and **Absent** over the **Last 30 days**, **Last 90 days** or **Last 365 days**. A link such as **2 unmarked** counts the reservations nobody marked yet. Select a number or **View reservations** to open the list. Use the menu at the top to show **All** reservations or one attendance state only. <figure> <img src="/images/guide/staff/member-attendance.png" alt="Reservations panel for Alex Morgan over the last 90 days, with the attendance menu set to All and outlined, listing one Place à la piscine reservation marked Present." width="1280" height="800" /> <figcaption> The past reservations of Alex Morgan over the last 90 days. The control to use is outlined in orange. </figcaption> </figure> ## 3. Follow the account activity [#3-follow-the-account-activity] ### Building entries [#building-entries] When a member arrives, they can select **I'm in the building** in their member area. In the **Activity** tab, the **Building entries** card lists the date and time of their 20 most recent entries, or **No entries yet.** <figure> <img src="/images/guide/staff/member-building-entries.png" alt="Building entries card listing four arrival dates and times." width="380" height="215" /> <figcaption> The last four arrivals of the example member Alex Morgan. </figcaption> </figure> ### Account activity [#account-activity] In the **Activity** tab, the **Account activity** card shows whether the member can sign in: * **Pending activation**: the member has not chosen a password yet. The card says whether an activation link is pending, expired or valid until a given date. Select **Send activation email** or **Resend activation email** to send them a new link. * Once activated, the card shows the **Last sign in** and **Active session** or **Signed out**. Select **Sign out member** and confirm to sign them out of every device, for example after a lost phone. They can sign in again at any time. <figure> <img src="/images/guide/staff/member-account-activity.png" alt="Account activity card with the last sign-in, an Active session label and a Sign out member button outlined in orange." width="380" height="184" /> <figcaption> The account activity of the example member Alex Morgan, who is signed in. The control to use is outlined in orange. </figcaption> </figure> ### Sponsorship and invited guests [#sponsorship-and-invited-guests] In the **Profile** tab, the **Sponsorship** card lists the person's **Sponsors** and the **Sponsored members** they brought in. A sponsor is recorded when your team accepts an [application](/en/guide#application) that used their sponsor code. Select a name to open that profile. When the member invited guests, an **Invited guests** card lists each guest with the start of their access and **Active**, **Ended** or **Revoked**. <figure> <img src="/images/guide/staff/member-invited-guests.png" alt="Invited guests card listing Clara Fontaine with the start of her access and an Active label." width="380" height="124" /> <figcaption> The guest invited by the example member Alex Morgan. </figcaption> </figure> ## 4. Read the history and signed documents [#4-read-the-history-and-signed-documents] Select the **History** tab. It shows: * **Membership history**: each plan **Added**, **Updated**, **Renewed** or **Removed**, with who did it, when, the plan before and after, and the reason. * **Status history**: each status change with its dates and reason. * **Profile history**: each edit of the person's details, additional information, photo, tags or note, with who did it and when. <figure> <img src="/images/guide/staff/member-history.png" alt="History tab of Alex Morgan's profile selected and outlined, with a Membership history entry for the Adhésion 51 IÉNA (2026) - Abonnement Membre 2026 plan marked Added on 1 September 2026, a Status history that says No events recorded yet, and a Profile history below them." width="1280" height="800" /> <figcaption> The history of Alex Morgan, whose plan was granted on 1 September 2026. </figcaption> </figure> Select an entry to see its details. In **Profile history**, an entry lists each field that changed with its value before and after. In the **Membership** tab, the **Signed documents** card lists the documents the person signed during their application, with their signing date. Select **PDF** to download one. If the card says **Downloads are unavailable until Yousign is configured.**, ask your Champagne provider to set up electronic signature. ## 5. Guests and external customers [#5-guests-and-external-customers] Some profiles belong to people who are not members: * A *guest* was invited for a limited time, by your team or by a member. Their profile has a **Guest access** card with **Invited by**, **Access started** and **Access ends**. In the People list, they show **Guest account**, then **Former guest** once their access has ended. * An *external customer* booked online without a membership. Their profile has an **External customer** card with the start and end of their access, and the list shows **External customer**. <figure> <img src="/images/guide/staff/guest-profile.png" alt="Guest access card with Invited by Alex Morgan, and the Access started and Access ends dates." width="380" height="208" /> <figcaption> The guest Clara Fontaine, invited by Alex Morgan. </figcaption> </figure> <figure> <img src="/images/guide/staff/external-customer-profile.png" alt="External customer card with the Access started and Access ends dates." width="380" height="128" /> <figcaption> The external customer Marc Lefort, who booked a suite online. </figcaption> </figure> Guest profiles have no **Sponsorship** card. To invite a guest, see [Manage members](/en/guide/staff/people#3-invite-a-guest). # Change a member's status Suspend a member, block their bookings, ban or disable them, and give access back. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/member-status A member's status decides whether they can come to the club and book. This page shows how to change it from the **Member status** card of the profile, how to plan a change for later and how to give access back. A status can also change on its own after no-shows, when your club set [no-show sanctions](/en/guide/admins/club-settings#3-set-the-reservation-rules). To open a profile, see [Work on a member's profile](/en/guide/staff/member-profile). | Status | What the member can do | | -------------------- | ----------------------------------------------------------------------- | | **Active** | Everything: normal access. | | **Suspended** | Nothing during the chosen period. Access comes back after the end date. | | **Bookings blocked** | Come to the club, without making reservations during the chosen period. | | **Banned** | Nothing, until your team lifts the ban. | | **Disabled** | Nothing, until your team re-enables the account. | ## 1. Change the status of an active member [#1-change-the-status-of-an-active-member] 1. In the **Member status** card, select **Change status**. 2. Choose **Suspend**, **Block bookings**, **Disable** or **Ban**. 3. Fill in the window: * **Suspend** and **Block bookings** ask for a start date and an end date. The start date is today or later. You may add a **Reason (optional)**. * **Ban** needs a **Reason**. The ban starts immediately. * **Disable** only asks you to confirm. 4. Select the button at the bottom of the window, for example **Suspend member**. <figure> <img src="/images/guide/staff/member-suspend.png" alt="Suspend member window for Alex Morgan with a suspension start date of today, an end date of 30 November 2026, an example reason and Cancel and Suspend member buttons, Suspend member outlined." width="576" height="436" /> <figcaption> Suspending the example member Alex Morgan until the end of November. The control to use is outlined in orange. </figcaption> </figure> ## 2. Plan a suspension or a booking block [#2-plan-a-suspension-or-a-booking-block] When the start date is in the future, the card says **Suspension scheduled from** or **Booking block scheduled from** with the dates. The member keeps normal access until then. Select **Cancel scheduled suspension** or **Cancel scheduled booking block** to call it off. ## 3. Give access back [#3-give-access-back] While a status is in force, the card shows its dates or its reason, and one button to end it at once: | Status | Button | | -------------------- | --------------------------- | | **Suspended** | **End suspension early** | | **Bookings blocked** | **End booking block early** | | **Banned** | **Lift ban** | | **Disabled** | **Re-enable member** | Selecting the button makes the member **Active** again straight away, without a confirmation window. <figure> <img src="/images/guide/staff/member-status-suspended.png" alt="Member status card saying Suspended from 1 September 2026 through 31 December 2026, with the reason and an outlined End suspension early button." width="380" height="224" /> <figcaption> The status of the example member Camille Roussel, suspended until the end of the year. </figcaption> </figure> Every status change is kept in the **Status history** of the profile. See [Read the history and signed documents](/en/guide/staff/member-profile#4-read-the-history-and-signed-documents). # Tags, notes and notifications Label a member with tags, keep an internal note about them and send them a notification. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/member-tags-and-notifications From a member's profile you can label them with tags, write a note for your team and send them a short notification. This page shows each of these tasks. To open a profile, see [Work on a member's profile](/en/guide/staff/member-profile). ## 1. Label a member with tags [#1-label-a-member-with-tags] Tags are labels only your team sees, such as "Juniors" or "Tournament players". Use them to find people in the list and to choose the recipients of a [newsletter](/en/guide/staff/newsletters). Select **Edit tags** in the **Tags** card. Tick a tag to add it, untick it to remove it. Each change is saved at once. You can also select the cross on a tag in the card to remove it. <figure> <img src="/images/guide/staff/member-tags.png" alt="Tag menu open on Alex Morgan's profile under the outlined Edit tags button, with a search field, the Founding member and Tournament players tags ticked, Juniors unticked, and a Manage tags link." width="1280" height="800" /> <figcaption> The three example tags of Ukab Clubs, two of them on Alex Morgan. The control to use is outlined in orange. </figcaption> </figure> To create a tag, type its name in **Search or create a tag…**. When no tag has this name, select **Create**. The dot next to it sets the **Tag color**; choose it before you create the tag. The new tag is added to the person straight away. <figure> <img src="/images/guide/staff/member-tag-create.png" alt="Tag menu with Volunteers typed in the search field, an outlined Create Volunteers button and a red color dot." width="289" height="125" /> <figcaption> Creating an example tag, Volunteers. </figcaption> </figure> To delete a tag from the whole club, point at it in the menu and select the bin icon, then **Delete**. The tag is removed from every person carrying it. **Manage tags** at the bottom of the menu opens the member settings, where an administrator renames and recolors tags. **Back to** followed by the person's name returns to their profile. It appears only if you can open **Settings**. See [Club settings](/en/guide/admins/club-settings). ## 2. Keep a note [#2-keep-a-note] The **Note** card holds an internal note about the person, for example a habit worth knowing at reception. Members do not see it. 1. Select **Edit note** in the **Note** card. 2. Type the note, up to 10,000 characters. The counter under the field shows how many you used. 3. Select **Save note**, or **Cancel** to leave the note as it was. <figure> <img src="/images/guide/staff/member-note.png" alt="Note card being edited with an example note about Alex Morgan, a character counter, and Save note and Cancel buttons, Save note outlined." width="380" height="263" /> <figcaption> Writing an example note on Alex Morgan's profile. </figcaption> </figure> **Edit note** appears only when your [role](/en/guide#role) has the **Edit staff notes** permission. ## 3. Send a notification [#3-send-a-notification] Use a notification for short practical news, for example a package waiting at reception. 1. Select **Send notification** at the top of the profile. 2. Choose a **Category**: **Received package**, **Lost object** or **Other**. The first two fill in a ready-made **Message** that you can change. **Other** starts empty. 3. Select **Send**. <figure> <img src="/images/guide/staff/send-notification.png" alt="Send notification window for Alex Morgan with the Received package category and its prefilled message, and Cancel and Send buttons, Send outlined." width="576" height="424" /> <figcaption> A Received package notification for Alex Morgan, with its ready-made message. </figcaption> </figure> The member receives it in the app and by email, according to their **Club messages** notification settings. See [Notifications](/en/guide/members/notifications). # Send newsletters Write a newsletter, choose who receives it, preview it and send it to members. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/newsletters The **Newsletters** section lets you write an email once and send it to all your members or to a group of them. This page shows how to write a newsletter, check how it looks, choose its recipients and send it. ## 1. Open the Newsletters section [#1-open-the-newsletters-section] In the left menu, select **Newsletters**. The list shows each newsletter with its subject, when it was last edited or sent, and its status. <figure> <img src="/images/guide/staff/newsletters.png" alt="Newsletters section with a New newsletter button outlined in orange, a Sending is not configured warning and one Draft newsletter, Autumn tournament: registrations are open, with its edit date and the Tournament players tag." width="1280" height="800" /> <figcaption> The Newsletters section of the example club Ukab Clubs, with one draft for the tournament players. New newsletter, outlined in orange, starts a new draft. </figcaption> </figure> Below the subject, each line shows the tags it is sent to. A newsletter for all members has no tag. | Status | What it means | | ----------- | ----------------------------------------------------------- | | **Draft** | The newsletter is being written. Only your team can see it. | | **Sending** | The emails are on their way. | | **Sent** | Sending is over. The newsletter can no longer change. | | **Failed** | Sending stopped. Open the newsletter to see why. | ### Notices about sending [#notices-about-sending] A notice at the top of the list and of each newsletter tells you when sending needs attention: | Notice | What it means and what to do | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sending is not configured** | Email sending is not switched on for your Champagne installation yet. You can still write drafts. Contact your Champagne provider. | | **Set up a sending domain** | Your club has no sending address yet. A club administrator sets one up: select **Open newsletter settings**. | | **The sending domain is not verified yet** | The sending address is waiting for its verification. Select **Open newsletter settings** to check it. See [Club settings](/en/guide/admins/club-settings#send-newsletters-from-your-own-address). | | **Test environment** | This installation is for testing. Newsletters only go to the addresses listed in the notice, or **Newsletters are not delivered to anyone.** Members outside the list receive nothing. | When no notice shows, sending is ready. ## 2. Write a newsletter [#2-write-a-newsletter] Select **New newsletter**. Champagne creates a draft and opens it. To continue a draft, select it in the list. The form sits on the left and the **Email preview** on the right. <figure> <img src="/images/guide/staff/newsletter-editor.png" alt="Newsletter draft Autumn tournament: registrations are open, with Recipients set to the Tournament players tag and 1 subscribed member, the Subject, Preview text and Message fields on the left and the rendered email on the right." width="1280" height="800" /> <figcaption> The example draft announcing the autumn tournament, with its email preview. </figcaption> </figure> 1. Enter the **Subject**, the title members see in their inbox. 2. Enter a **Preview text**: the short line most inboxes show after the subject. 3. Write the **Message**. Use the buttons above the box to add a **Heading**, **Bold** or **Italic** text, a **Link**, a **Bulleted list** or **Numbered list**, a **Quote**, a **Divider**, an **Image** or a **Button**. 4. Select **Save** at the bottom of the form. The preview updates as you type and shows the email as members will receive it, with your club's name at the top. ## 3. Choose the recipients [#3-choose-the-recipients] By default, **Recipients** is set to **All members**. To write to a group only, select **Choose tags** and tick one or more tags. The newsletter then goes to members who have at least one of these tags. Select **Add** to pick more tags, or select the cross on a tag in the field to widen the audience again. <figure> <img src="/images/guide/staff/newsletter-tags.png" alt="Tag menu open below Recipients, with a search field, the Founding member, Juniors and Tournament players tags, Tournament players ticked, and a Manage tags link." width="1280" height="800" /> <figcaption> Choosing the tags of the example tournament newsletter. The control to use is outlined in orange. </figcaption> </figure> The line below the field counts the members who will receive it, for example **3 subscribed members**. Only members with a current membership receive newsletters. Members who unsubscribed, and **Banned** or **Disabled** accounts, are left out. **Suspended** members still receive them. > Tags are labels you put on member profiles in **People**, such as "Juniors" or "Tournament > players". To create one from here, type its name in **Search or create a tag…** and select > **Create**: the tag is added to your club and to this newsletter. See [Tags, notes and > notifications](/en/guide/staff/member-tags-and-notifications#1-label-a-member-with-tags). ## 4. Test and send [#4-test-and-send] When sending is set up and the draft has a subject and a message: 1. Select **Send test** to receive the newsletter yourself first. Tick up to five addresses of your club's team, select **Send test** and check the email in your mailbox. 2. Select **Send**. A window asks you to confirm the number of recipients, for example **Send to 3 recipients?**. 3. Select **Send** in that window. Unsaved changes are saved first. > Once sent, a newsletter cannot be edited or recalled. After sending, open the newsletter to follow its delivery: how many emails were **Delivered**, **Bounced** back, **Unsubscribed** or **Marked as spam**. **Not sent** counts the emails that could not leave. If sending stops, the newsletter shows **Failed** and a **Sending failed** notice with the reason. You can then correct it and select **Send** again. To remove a draft you no longer need, open it and select **Delete**. The **Delete this draft?** window says the draft will be permanently deleted. Select **Delete draft** to confirm, or **Cancel** to keep it. ## 5. When a colleague edits the same draft [#5-when-a-colleague-edits-the-same-draft] If someone else saves the draft while you have unsaved changes, a **This newsletter changed** window opens: * **Reload** shows their version. Your unsaved changes are lost. * **Keep editing** keeps your text. When you save, it replaces their version. When you have no unsaved changes, the draft updates on its own. # Manage members Find a member, open their profile and update their details. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/people The **People** section lists every member and customer of your club. From there you can find someone, add a new person, invite a guest for a few hours and open a profile. ## 1. Find a person [#1-find-a-person] In the left menu, select **People**. The list shows each person's **Name**, **Email** and **Status**. Select a column title to sort the list by that column. <figure> <img src="/images/guide/staff/people.png" alt="People section with a search field, Add person, Guest invitations and Filters buttons, and a list of members with their email and a status such as Active, Suspended or Disabled." width="1280" height="800" /> <figcaption> The members of the example club Ukab Clubs, sorted by name. </figcaption> </figure> An active member who has not chosen a password yet shows **Pending activation**. Some people also show a second label next to their status: | Label | Who it is | | --------------------- | --------------------------------------------------------------- | | **Guest account** | A guest invited for a few hours, whose access is still running. | | **Former guest** | A guest whose access has ended. | | **External customer** | Someone who booked online without a membership. | Type part of a name or email address in **Search by name or email…**. The list updates as you type. When nobody matches, select **Clear search and filters** to see everyone again. To narrow the list further, select **Filters**, choose your criteria and select **Apply filters**. You can filter by: | Filter | What it keeps | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | **Status** | **Active**, **Suspended**, **Bookings blocked**, **Banned** or **Disabled** members. | | **Membership** | People with a **Valid membership**, **Expired memberships only**, **Any membership record** or **No membership records**. | | **Type** | An **Individual** or a **Company**. | | **Gender** | The gender recorded on the profile. | | **Minimum age**, **Maximum age** | People within an age range. | | **Tags** | People with at least one of the chosen tags, or **Only people without any tag**. | | **Attributes** | The extra questions your club records, such as a preferred language or a profession. | **Tags** and **Attributes** appear only when your club has tags or extra questions. <figure> <img src="/images/guide/staff/people-filters.png" alt="Filters dialog with Status, Membership, Type, Gender, Minimum age and Maximum age fields, a Tags field, an Attributes group with Preferred language, Printed newsletter and Profession, and Reset, Cancel and Apply filters buttons." width="1280" height="800" /> <figcaption> The filters of Ukab Clubs, including three example attributes. The control to use is outlined in orange. </figcaption> </figure> The **Filters** button shows how many filters are active. Select **Reset** in the dialog, then **Apply filters**, to remove them all. ## 2. Add a person [#2-add-a-person] Select **Add person**, enter the **First name**, **Last name** and **Email**, then select **Create person**. The person receives an email with a link to choose their password. <figure> <img src="/images/guide/staff/add-person.png" alt="Add a person dialog filled with the first name Camille, the last name Martin and the email camille.martin@example.com, with Cancel and Create person buttons." width="1280" height="800" /> <figcaption> Adding an example member, Camille Martin. </figcaption> </figure> ## 3. Invite a guest [#3-invite-a-guest] A guest is someone who gets a temporary account for a few hours, for example a visitor coming for an event. Select **Guest invitations**. 1. Enter the **Guest email**. 2. Choose the **Access duration**: **12 hours**, **24 hours** or **48 hours**. The time starts when the guest accepts the invitation. 3. Select **Send invitation**. The guest receives an email to accept it. <figure> <img src="/images/guide/staff/guest-invitations.png" alt="Guest invitations dialog with the email guest@example.com, the access duration set to 24 hours, and a Sent invitations list with two pending invitations and the active guest access of Clara Fontaine." width="1280" height="800" /> <figcaption> Inviting an example guest for 24 hours. </figcaption> </figure> The **Sent invitations** list in the same dialog shows each invitation with its state: **Invitation pending**, **Invitation expired**, **Guest access active** or **Guest access ended**. > **Add person** and **Guest invitations** appear only if your [role](/en/guide#role) includes the > **Create and edit member profiles** permission. Ask your administrator if you need them, and see > [Who sees what](/en/guide/admins/who-sees-what) for the other permissions. ## 4. Open a member's profile [#4-open-a-members-profile] Select a person in the list to open their profile. There you update their details, [plans](/en/guide#plan), status and tags, send them a notification and follow their activity. See [Work on a member's profile](/en/guide/staff/member-profile). > Select a tag in the People list to show only the people who have it. Select **Back to People** to return to the list with your search and filters unchanged. # Use the club calendar Read the day at a glance, manage sessions and work with floor plans. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/planning **Club calendar** is the calendar of the [reservation categories](/en/guide#reservation-category) you work in, such as group classes, private lessons or pool loungers. It lets you see who is booked, follow the places left in each session, mark attendance, manage a session and find a free spot on a floor plan. Your administrator chooses which categories appear here. ## 1. Read the week or the day [#1-read-the-week-or-the-day] Open **Club calendar** in the left menu and select a category in the strip at the top. <figure> <img src="/images/guide/staff/planning-week.png" alt="Club calendar for the Cours collectif category in Week view: the Main studio row shows each day a 09:00 session marked Past or Session full, an Open studio session at 12:30, and a +2 sessions link." width="1280" height="800" /> <figcaption> A week of the example group classes Cours collectif in the Main studio, with the daily Open studio. The control to use is outlined in orange. </figcaption> </figure> * **Week** shows seven days side by side. **Day** shows the hours of a single day. Select a day name in the header to open that day. * The arrows move to the previous or next period, and the calendar icon brings you back to today. * **Search...** finds a booking by name. * In group classes, each block is a session. It shows its start time and **Session full**, the number of places left, or **Past** once it is over. A session with its own name, such as **Open studio**, shows that name. In **Week**, each day shows its first two sessions as cards. When a day has more, select a link such as **+2 sessions** to open that day and see them all. Bookings work like in the **Reservations** section: select one to open it, or hover over a free time and select **+** to add one if your [role](/en/guide#role) allows it. See [Handle reservations](/en/guide/staff/reservations). ## 2. Follow a session and mark attendance [#2-follow-a-session-and-mark-attendance] Select a session to open its panel. **Booked places** shows how full it is when the session has a limit, and **Participants** lists who is coming and how many places each person booked. <figure> <img src="/images/guide/staff/planning-session.png" alt="Session panel for tomorrow's Cours collectif from 09:00 to 10:00 in the Main studio, marked Session full, with 8 of 8 places booked, six participants including Louis Garnier with 3 places, the start of the Waitlist and a disabled Add participant button." width="1280" height="800" /> <figcaption> The example 9:00 Cours collectif, full with six participants and three members waiting. </figcaption> </figure> * From the day of the session, mark each participant **Present**, **Late** or **Absent**. **Late** and **Absent** open once the session starts. **Mark remaining present** marks everyone you have not marked yet in one go. * **Add participant** books a member into the session while places remain, until the session starts. It needs the **Create and change reservations** permission, and marking attendance needs **Validate attendance and tickets**. * The icon at the end of a participant's line removes them from the session. Their reservation is canceled and any payment is refunded, so a confirmation window asks first. This icon needs the **Create and change reservations** permission. When the cancellation deadline has passed, the window offers **Refund the price**: leave it unticked to keep the price and return only the deposit. * When members are waiting for a place, **Waitlist** lists them in order, with the date they joined. A place that frees up goes to them automatically, in that order. <figure> <img src="/images/guide/staff/planning-waitlist.png" alt="The same session panel scrolled down: six participants, then a Waitlist of 3 with Sarah Cohen #1, Adam Diallo #2 and Alex Morgan #3, each in the Main studio with the date they joined." width="1280" height="800" /> <figcaption> Three members waiting for a place in the example full session. </figcaption> </figure> Labels under the session's title show **In progress** or **Past**, **Session full**, and **Open access** for sessions people attend without booking, such as the example **Open studio**. Open access sessions have no **Booked places** meter and no participant list. > When members get a QR code ticket for the category, **Tickets** above the calendar checks it, and > **Mark present** records their arrival. See [Check guests in and > out](/en/guide/staff/check-in-and-deposits). ## 3. Change, export or cancel a session [#3-change-export-or-cancel-a-session] Select the **⋯** button next to the session's title to open **Session actions**. The choices depend on the session and on your role. <figure> <img src="/images/guide/staff/planning-session-actions.png" alt="Session panel of the full 09:00 Cours collectif with the Session actions menu open, offering Edit session, Export participants, Export waitlist and Cancel session." width="1280" height="800" /> <figcaption> The actions of the example full Cours collectif session, seen by an administrator. </figcaption> </figure> * **Edit session** changes the session's details, such as its time, **Location** or **Member limit (optional)**. It appears before the session starts, for staff who manage the category. See [Sessions, guest lists and blocked periods](/en/guide/admins/sessions-and-blocked-periods). * **Export participants**, when the session has participants, downloads a spreadsheet file (CSV) with each participant, their reservation status, attendance and number of places. * **Export waitlist**, when people are waiting for a place, downloads them as a CSV file too. * **Cancel session** cancels it for everyone before it starts. It needs the **Create and change reservations** permission. A window asks **Cancel this session?**: members are notified and paid reservations are refunded. Select **Cancel session** to confirm, or **Keep session** to leave it. ## 4. Use the floor plan [#4-use-the-floor-plan] Some categories, such as pool loungers, have a floor plan. It appears under the calendar, below **Floor plan**. When a category has several plans, select a plan's name above the drawing to switch. <figure> <img src="/images/guide/staff/planning-floor-plan.png" alt="Day view of the Place à la piscine category with a vertical time line, and below it the Piscine extérieure floor plan showing a pool, plants, a parasol and five loungers named Transat #1 to Transat #5." width="1280" height="800" /> <figcaption> The example outdoor pool plan Piscine extérieure with its five loungers. </figcaption> </figure> The plan shows each place at the time marked by the vertical line in the calendar. Drag that line to another time to update the plan. Places that are reserved or unavailable at that time appear grayed out. Select a free place to open the reservation form for that place and time, if your role allows you to add bookings. <figure> <video src="/videos/guide/staff/planning-time-cursor.webm" aria-label="The time cursor is dragged to 17:45, over Louis Garnier's booking: Transat #3 turns gray on the floor plan below, then the cursor moves back." width="835" height="640" /> <figcaption> Moving the time cursor over a booking grays out its lounger on the plan. </figcaption> </figure> > Staff who manage the category can select **Edit floor plan** to arrange the layout. The [AI tools > for your floor plan](/en/ai-flows) guide explains how to [import an existing > plan](/en/ai-flows/plan-import) and [create new objects](/en/ai-flows/artwork-generation). # People, products and customer sheets on a booking Invite someone to a booking, mark attendance, add extras and keep a customer sheet. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/reservation-people The page of a reservation gathers what you need to know about the customer: the people on the booking, whether they came, the extras they ordered and, for some categories, their customer sheet. This page shows how to work with each of them. To open a reservation, see [Handle reservations](/en/guide/staff/reservations#4-open-move-or-cancel-a-reservation). **Invite** and **Remove** need the **Create and change reservations** permission. Attendance marks need **Validate attendance and tickets**. When your [role](/en/guide#role) lacks a permission, the matching buttons do not appear. ## 1. Work with the people on a booking [#1-work-with-the-people-on-a-booking] **People on this reservation** lists the **Booking contact** and the people they invited, with the count of people against the places booked. Each invited person is marked **Invited**, **Accepted** or **Declined**. **Account linked** means the email belongs to a member of your club. * To invite someone, select **Invite**, enter their **Email** and select **Send invitation**. The person receives an email to accept or decline. When your club lets guests use a temporary account, choose its **Access duration**: 12, 24 or 48 hours. The temporary account starts when the guest accepts. * To take someone off the booking, select **Remove** on their line. <figure> <img src="/images/guide/staff/invite-someone.png" alt="Invite someone window with friend@example.com in the Email field, a warning that the reservation is full and that inviting someone adds one person to it, and the buttons Close and Send invitation, Send invitation outlined." width="384" height="292" /> <figcaption> Inviting a friend to the example booking of Alex Morgan on Transat #4, which is already full. The control to use is outlined in orange. </figcaption> </figure> When every place is taken, the window warns you that the invitation adds one person to the booking. ## 2. Mark attendance [#2-mark-attendance] **Attendance** records whether the customer came. Select **Present**, **Late** or **Absent**. **Late** and **Absent** open once the reservation starts. Type in **Add a note (optional)** before you select a mark to save a note with it. The last mark shows who recorded it and when. The marks add up in the **Attendance** card of the member's profile. See [Check attendance](/en/guide/staff/member-profile#2-check-attendance). > **Absent** counts as a no-show. When your club has no-show sanctions, enough no-shows warn, > suspend, block or ban the member at once, and the member is told. Check the mark before you save > it. See [Set the reservation rules](/en/guide/admins/club-settings#3-set-the-reservation-rules). ## 3. Add extras to a booking [#3-add-extras-to-a-booking] When the resource offers extras, such as drinks, the **Add reservation** form lists them under **Products** on the right. Use **+** and **−** to set the quantity of each one. **Product total** shows what they cost. Extras marked **Staff only** are hidden from members. <figure> <img src="/images/guide/staff/reservation-products.png" alt="Add reservation form for the Transat #2 lounger with a Products column listing Sparkling water at 3.5 Stars, a quantity of 2 with its + button outlined, and a Product total of 7 Stars." width="896" height="604" /> <figcaption> Two bottles of the example Sparkling water added to a pool lounger booking. </figcaption> </figure> **Product charges** appears on the reservation page when extras were added to the booking, such as minibar drinks charged from **Housekeeping**. Each line shows the product, the quantity and the amount. <figure> <img src="/images/guide/staff/reservation-product-charges.png" alt="Product charges card listing Welcome champagne × 1 at 65 Stars, Sparkling water × 2 at 7 Stars and Champagne bottle × 1 at 65 Stars." width="1044" height="162" /> <figcaption> The extras charged during the example stay of Jamie Chen in the Garden suite. </figcaption> </figure> ## 4. Keep a customer sheet [#4-keep-a-customer-sheet] Some [reservation categories](/en/guide#reservation-category), such as beauty treatments, keep a customer sheet. Your administrator switches this on per category. The **Customer sheet** card then appears on each reservation in that category and in the **Activity** tab of the profile of each person who booked in it, with one tab per category when there are several. The sheet shows: * **Contraindications**, highlighted in red when filled in, such as an allergy. * **Preferences** and a **Note**. * **Visits**: the number of past visits and the latest ones, with their date, time, resource and attendance. Select **Show all** to see up to ten, and select a visit to open the reservation. <figure> <img src="/images/guide/staff/customer-sheet.png" alt="Customer sheet card for the Soin en salon de beauté category with no contraindications recorded, empty Preferences and Note, 17 visits with the three latest listed, and an outlined Edit sheet button." width="645" height="321" /> <figcaption> The customer sheet of the example member Zoé Lefèvre, with her past beauty treatments. </figcaption> </figure> To fill it in: 1. Select **Edit sheet**. 2. Enter the **Preferences**, **Contraindications** and **Note**. 3. Select **Save sheet**. <figure> <img src="/images/guide/staff/customer-sheet-edit.png" alt="Customer sheet being edited with an example preference, an example nut allergy in Contraindications, an empty Note and Save sheet and Cancel buttons, Save sheet outlined." width="645" height="452" /> <figcaption> Recording an example allergy on Zoé Lefèvre's sheet. </figcaption> </figure> The sheet is the same on the profile and on each of the person's reservations in that category. Team members who are not administrators can edit it when they are assigned to the resource of one of the person's reservations, until one day after that reservation ends. # Propose a time for a request Answer a member's request for a private lesson by proposing a time. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/reservation-requests In some categories, such as private lessons, members send a request and the club suggests the time. This page shows how to find these requests, propose a time and change or withdraw your proposal. ## 1. Find the requests [#1-find-the-requests] The requests appear above the calendar of their category in **Reservations**, under **Requests awaiting a time**. Select **Open** to read one. Coaches also find them in **My reservations** and in **Club calendar**. See [Your reservations and availability](/en/guide/staff/schedule-and-availability). A request shows **Pending** and a **Reservation request** card on its page. Members can write to you in the **Conversation** below. See [Answer messages](/en/guide/staff/chats). <figure> <img src="/images/guide/staff/reservation-request.png" alt="Reservation page of Alex Morgan's Yoga with Thomas request, marked Pending, with a Reservation request card holding a Propose a time button, and the start of a conversation where Alex asks for a yoga lesson on weekday evenings." width="1280" height="800" /> <figcaption> The example request Yoga with Thomas, seen by the coach account Sport coach. The control to use is outlined in orange. </figcaption> </figure> ## 2. Propose a time [#2-propose-a-time] 1. Select **Propose a time**. 2. Enter the **Start date**, **Start time**, **End date** and **End time**. When you change the start, the end moves with it to keep the same length. 3. Turn on **Offer this session for free** to waive the price. The option appears when the session has a price. 4. Select **Send proposal**. The member sees the proposed time on their reservation and can accept it. <figure> <img src="/images/guide/staff/propose-a-time.png" alt="Propose a time window with a start and end date, a start time of 18:00, an end time of 19:00, Offer this session for free turned on and a Send proposal button." width="448" height="348" /> <figcaption> Proposing a free one-hour yoga session at 18:00. </figcaption> </figure> ## 3. Change or withdraw a proposal [#3-change-or-withdraw-a-proposal] Once sent, the card shows the proposed time and the **Total**, marked **Free session** when you waived the price. **Confirmed on acceptance.** means the booking is confirmed as soon as the member accepts. **Payment due after acceptance.** means the member pays after accepting. <figure> <img src="/images/guide/staff/reservation-proposal.png" alt="Reservation request card with a proposed time from 18:00 to 19:00, a Free session label, a total of 0 Stars, the line Confirmed on acceptance and a Change proposal button." width="1044" height="176" /> <figcaption> The proposal sent for the example request Tennis with Julien. </figcaption> </figure> * **Change proposal** sends a new time in place of the previous one. * **Withdraw proposal** appears when you propose a new time for a booking that already has one. It takes the proposal back, and the booking keeps its current time. While a request waits for a time, **Cancel** is the only status button. In these categories, **Edit** is only available while the request is **Pending**. # Handle reservations See, find, create, move and cancel reservations in the calendar. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/reservations The **Reservations** section shows every booking of your club in a calendar, one [reservation category](/en/guide#reservation-category) at a time. A reservation category groups what members can book, such as a restaurant, a meeting room or group classes. Each bookable item inside a category, such as a table or a room, is called a resource. This page shows how to read the calendar, find a booking, add one, change its time and cancel it. Other tasks on reservations have their own pages: * [People, products and customer sheets on a booking](/en/guide/staff/reservation-people): invite someone, mark attendance, add extras and keep a customer sheet. * [Propose a time for a request](/en/guide/staff/reservation-requests): answer the requests of categories such as private lessons. * [Blocked periods, sessions and waitlists](/en/guide/staff/blocked-periods-and-waitlists): close a resource for a while and see who is waiting for a place. ## 1. Pick a category and a period [#1-pick-a-category-and-a-period] Open **Reservations** in the left menu. The strip at the top lists the categories of your club. Select one to show its calendar. When the list is long, use the arrows at each end of the strip, or select the magnifying glass and type part of a category name. <figure> <img src="/images/guide/staff/reservations-calendar.png" alt="Reservations calendar for the Salle de réunion category in Week view: the Conference room row shows one booking per day at 13:00 and a blocked period on Monday." width="1280" height="800" /> <figcaption> A week of the example meeting room Salle de réunion at Ukab Clubs. The control to use is outlined in orange, and the numbers match the list below. </figcaption> </figure> 1. Each row is a resource, here the **Conference room**. Each colored block is a booking with its start time and the customer's name. A red block marked **Blocked** is a period when nobody can book. 2. **Week** and **Day** switch between a whole week and a single day with hours. In **Week**, each day shows its first two bookings as cards, and a link such as **+1 reservation** opens the day when it has more. Bookings over several days, such as hotel stays, span their days instead. Categories with a floor plan show the week as a time line. Select a day name in the header to open that day directly. 3. The arrows on each side of the dates move to the previous or next period. Select the dates to pick a day in a calendar, and the calendar icon to come back to today. 4. The buttons on the right depend on the category and on your [role](/en/guide#role). **Category settings** changes how the category works, and your administrator sets it up. See [Set up reservation categories](/en/guide/admins/reservation-categories). **Block periods**, **Manage sessions** and **Waitlist** are explained in [Blocked periods, sessions and waitlists](/en/guide/staff/blocked-periods-and-waitlists). **Tickets** checks members' QR code tickets at the door. See [Check guests in and out](/en/guide/staff/check-in-and-deposits). > For private lessons, a member can ask for a session without choosing a time. These requests appear > above the calendar under **Requests awaiting a time**. Select **Open** to read the request and > propose a time. See [Propose a time for a request](/en/guide/staff/reservation-requests). ## 2. Find a booking [#2-find-a-booking] Type a name in **Search...** above the calendar. The list under the field shows each matching booking with its date and time. Select one to open it. <figure> <img src="/images/guide/staff/reservations-search.png" alt="Calendar search with a first name typed and the matching Conference room booking with its date and time, followed by an Add reservation button." width="1280" height="800" /> <figcaption> Searching the example Salle de réunion calendar for a member's first name. </figcaption> </figure> ## 3. Add a reservation [#3-add-a-reservation] Hover over a free time in the calendar and select the **+** that appears. The **Add reservation** form opens with the resource and time you picked. You can also select **Add reservation** at the bottom of the search list: the form then opens on the day shown at 9:00, or in a minute when that day is today and 9:00 has passed, so check the resource and the time. Adding a reservation needs the **Create and change reservations** permission. <figure> <img src="/images/guide/staff/reservation-form.png" alt="Add reservation form with Reservation selected, an empty Customer field, the Conference room resource, 1 person, start and end dates and times, and Close and Save reservation buttons, Save reservation outlined in orange." width="512" height="528" /> <figcaption> The form for a booking of the example Conference room, from 16:00 to 17:00. </figcaption> </figure> 1. In **Customer**, start typing a member's name and pick them in the list, or type the name of a guest who is not a member. 2. Check the **Resource** and the **Number of people**. 3. Adjust the **Start date**, **Start time**, **End date** and **End time**. 4. Select **Save reservation**. The booking appears in the calendar. A booking with a price or a deposit stays **Booked · payment due** until the customer pays from their account. Champagne cannot record a payment taken at the desk. When the resource offers extras, such as drinks, the form also lists them under **Products**. See [Add extras to a booking](/en/guide/staff/reservation-people#3-add-extras-to-a-booking). The **Blocked period** tab at the top of the form closes a resource for a while. See [Block a period](/en/guide/staff/blocked-periods-and-waitlists#1-block-a-period). ## 4. Open, move or cancel a reservation [#4-open-move-or-cancel-a-reservation] Select a booking in the calendar to open its page. It shows the status, the start and end, where the booking came from, the people on it, their attendance and the history of changes. **Back to** followed by the category name returns to the calendar. When the booking belongs to a person in **People**, **Open person** opens their page; **Back to Reservation** brings you back. <figure> <img src="/images/guide/staff/reservation-detail.png" alt="Reservation page for Sam Rivera, Confirmed, in the Conference room, with Edit and Cancel buttons, start and end times, the people on the reservation, attendance and history." width="1280" height="800" /> <figcaption> The reservation of the example member Sam Rivera in the Conference room. </figcaption> </figure> **Edit** and **Cancel** need the **Create and change reservations** permission. When your role lacks the permission, the buttons do not appear. To move a booking, select **Edit**. Change the **Resource**, the **Number of people** or the dates and times, then select **Save reservation**. If the new time or place changes the price of a confirmed booking, the difference is taken from or given back to the customer's credit balance, even when they paid by card. When their balance is too low for a higher price, the change is refused and they must top up first. A booking made for a typed name, or still waiting for payment, only changes its price. To cancel a booking, select **Cancel**. A window sums up the booking. Select **Cancel reservation** to confirm, or **Keep reservation** to leave it as it is. > When a paid booking is canceled, the customer is refunded automatically. If the cancellation > deadline has passed, the window offers **Refund the price** followed by the price: leave it > unticked to keep the price and return only the deposit. The buttons at the top of the page depend on the status. | Status | What you can do | | ------------------------ | -------------------------------------------------------------------------------------------------------------------- | | **Pending** | **Confirm** accepts the booking. **Book** appears instead when the customer still has to pay. **Cancel** refuses it. | | **Booked · payment due** | **Cancel**. The booking becomes **Confirmed** once the customer pays. | | **Confirmed** | **Cancel**. | | **Completed** | Nothing. The booking can no longer be changed. | | **Canceled** | Nothing. The booking can no longer be changed. | Each button opens a window that sums up the booking and asks you to confirm. The rest of the reservation page lists the people on the booking, their attendance and the extras charged. See [People, products and customer sheets on a booking](/en/guide/staff/reservation-people). # Finance Find an invoice, check what was paid and issue a credit note. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/sales-and-invoices The **Finance** section lists the invoices of your club: memberships, subscriptions and one-off charges. This page shows how to find an invoice, read it and issue a credit note when a member should pay less. The section appears when your [role](/en/guide#role) includes the **Manage invoices and credit notes** permission. ## 1. Find an invoice [#1-find-an-invoice] In the left menu, select **Finance**. The list shows the most recent invoices first, with their **Invoice** number, **Client**, **Issued** date, **Due date**, **Total** and **Status**. <figure> <img src="/images/guide/staff/invoices.png" alt="Finance section listing four invoices for Sam Rivera, Émilie Roche, Hugo Lemaire and Jamie Chen, with their totals and statuses Overdue, Paid and Voided." width="1280" height="800" /> <figcaption> Example invoices of Ukab Clubs, found by searching for example.com. </figcaption> </figure> * Type in the search box to find an invoice by number, client or email. * Select **Filters** to show one **Status** only, or the invoices issued between a **From** and a **To** date. Select **Apply filters** to update the list, and **Clear filters** to see everything again. * Select a column title to sort the list by that column. | Status | What it means | | ----------------- | ------------------------------------------------------- | | **To pay** | The invoice is waiting for payment. | | **Paid** | The invoice is fully paid. | | **Overdue** | The due date has passed and the invoice is still due. | | **Draft** | The invoice is being prepared and has no number yet. | | **Voided** | The invoice was cancelled and no longer has to be paid. | | **Uncollectible** | The club no longer expects this invoice to be paid. | When part of an invoice was credited, the amount followed by **credited** appears under its total, for example €1,800.00 credited. ### When the list is empty [#when-the-list-is-empty] | Message | What it means | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Billing is not connected yet** | Your club has no billing account yet. If you can open **Settings**, select **Connect billing**. Otherwise, ask a club administrator. | | **No invoices found** | Nothing matches your search or filters: select **Clear search and filters**. Without search or filters, no subscription has been billed yet. | | **No local invoices to show yet** | Champagne is still copying the invoices from your billing account. More appear as it goes. | Champagne keeps a copy of your billing account's invoices. While it updates this copy, the list says **Invoice synchronization in progress**. If it says **Invoice synchronization failed**, the list may be out of date. Tell your Champagne provider if the message stays. See [Payments](/en/guide/admins/payments) to connect a billing account. ## 2. Read an invoice [#2-read-an-invoice] Select an invoice to open it. <figure> <img src="/images/guide/staff/invoice.png" alt="Paid invoice for Jamie Chen showing the billed client, the Adhésion 51 IÉNA membership line, a received payment, the invoice total, an Adjust this invoice card and a Download PDF button outlined in orange." width="1280" height="800" /> <figcaption> A paid example membership invoice for Jamie Chen. The control to use is outlined in orange. </figcaption> </figure> * **Billed to** shows the client and their email. **Associated profile** shows the member it belongs to; select **Open person** at the top to open their page in **People**. * Below them, the invoice says whether it is a **Subscription invoice** or a **One-off invoice**, whether a membership is recorded on the member's profile, and the name of the issuer under **Issued by**. * The invoice lines list each item with its quantity and amount. A membership line also shows its price and period. A **Prorated adjustment for …** line corrects the price when a [plan](/en/guide#plan) changed during a billing period. * **Payments** lists each payment with its state: **Payment received** with its date and amount, or **Payment pending** and **Payment cancelled** with the amount asked, in gray. * **Credit notes** lists the credit notes already issued for this invoice. * **Invoice total** shows the **Subtotal**, the **Total excl. tax (HT)**, the tax (**TVA**), the **Total incl. tax (TTC)**, what was **Paid** and **Credited**, and, while the invoice is due, the **Remaining** amount and its **Due** date. Select **Download PDF** to save a copy of the invoice. When the invoice still has to be paid, the **Payment link** button opens its online payment page in a new tab. ## 3. Issue a credit note [#3-issue-a-credit-note] A credit note is a document that cancels all or part of an invoice. Use it to correct a mistake or to make a commercial gesture. 1. Open the invoice and select **Create credit note** in the **Adjust this invoice** card. 2. Check the amount in the question, for example **Credit €1,800.00 against** the invoice number. 3. Enter a **Reason**, up to 500 characters. It is required and kept with the credit note. 4. Select **Confirm credit note**. <figure> <img src="/images/guide/staff/credit-note.png" alt="Create credit note dialog asking to credit €1,800.00 against the invoice, with an example reason and Cancel and Confirm credit note buttons." width="1280" height="800" /> <figcaption> A credit note being prepared on the example invoice of Jamie Chen. </figcaption> </figure> The credit note covers the whole amount that can still be credited. It reduces what the client owes. Any part they already paid goes to their balance and is used for their future invoices. > Issuing a credit note does not send money back to the client's card and does not cancel their > membership. Once issued, the credit note appears in **Credit notes** with its number, date, reason and amount. Select **Download credit note** to save it as a PDF. A cancelled credit note shows **· Voided** after its number. <figure> <img src="/images/guide/staff/credit-notes.png" alt="Credit notes card listing credit note EFAVXZ3T-0006-CN-01 for €450.00 with its date, with the reason Avoir partiel de démonstration and a Download credit note link." width="645" height="136" /> <figcaption> A partial example credit note on Émilie Roche's membership invoice. </figcaption> </figure> When the whole invoice is credited, the **Adjust this invoice** card no longer appears. # Your reservations and availability Check the reservations made with you and set the hours you can work. Source: https://docs.champagne.dev-ukab.fr/en/guide/staff/schedule-and-availability Coaches and other staff who take bookings in their own name have two personal sections. **My reservations** lists the reservations made with you. **My availability** holds the hours you can work, so members can only book you during those hours. ## 1. Check your reservations [#1-check-your-reservations] Open **My reservations** in the left menu. Your upcoming reservations are grouped by day, starting with **Today** and **Tomorrow**. Each line shows the time, the customer's name, what they booked and the status of the booking. <figure> <img src="/images/guide/staff/my-schedule.png" alt="My reservations page with a Requests awaiting a time card listing two requests from Alex Morgan, followed by today's bookings with their times, customer names, Tennis with Julien or Yoga with Thomas, and a Booked · payment due status." width="1280" height="800" /> <figcaption> The reservations of the example coach account Sport coach at Ukab Clubs. </figcaption> </figure> | Status | What it means | | ------------------------ | ---------------------------------------------------- | | **Pending** | The booking waits for the club's decision. | | **Booked · payment due** | The place is held and the customer still has to pay. | | **Confirmed** | The booking is confirmed. | | **Completed** | The booking is over. | | **Canceled** | The booking was canceled. | Select a line to open the reservation. Select **Load more** at the bottom to see bookings further ahead. > When a member asks for a session without choosing a time, the request appears at the top under > **Requests awaiting a time**. **Awaiting your proposal** means you need to suggest a time: open > the request and select **Propose a time**. **Proposal sent** means the member has your proposal > and has not answered yet. See [Propose a time for a request](/en/guide/staff/reservation-requests). ## 2. See your hours for the week [#2-see-your-hours-for-the-week] Open **My availability**. The grid shows one week, with your working hours in green. A block marked **Off** is time off for that date. Use the arrows next to the dates to change week, and the calendar icon to come back to the current week. <figure> <img src="/images/guide/staff/my-availability.png" alt="My availability page for a week, with green blocks from 08:00 to 12:00 and 14:00 to 21:00 from Monday to Saturday and 10:00 to 12:00 on Sunday, and an Add hours button." width="1280" height="800" /> <figcaption> A week of working hours of the example coach account Sport coach. </figcaption> </figure> ## 3. Add or remove hours [#3-add-or-remove-hours] To add hours, select **Add hours**, or drag down an empty part of a day in the grid. Enter the **Date**, **Start time** and **End time**, then choose how far the change goes: * **Only** followed by the date changes that single day. * **Every** followed by the weekday changes that day in every week. <figure> <img src="/images/guide/staff/availability-add-hours.png" alt="Add hours window with a date, a start time of 12:00, an end time of 14:00, and the buttons Cancel, Only followed by the date and Every followed by the weekday, the last two outlined." width="512" height="292" /> <figcaption> Adding the example hours 12:00 to 14:00 on the current day. The controls to use are outlined in orange. </figcaption> </figure> To remove hours, select a green block. The **Remove hours** window opens with its times filled in. Shorten the times to remove only part of the block, then choose **Only** that date or, for weekly hours, **Every** that weekday. Removing hours for a single date adds time off for that day. To cancel time off, select the **Off** block and confirm **Remove time off?**. Your usual hours apply again on that date. > The club refuses a booking with you outside your available hours, and shows that you are not > available at this time. # Présentation Une aide pas à pas pour l’équipe du club, les membres et les admins du club. Source: https://docs.champagne.dev-ukab.fr/fr/guide Ce guide montre comment utiliser Champagne au quotidien. Chaque page détaille une tâche, nomme les boutons et les menus exactement comme ils s’affichent à l’écran et montre une capture de la vraie interface remplie de données d’exemple. ## Trouvez votre rubrique [#trouvez-votre-rubrique] [Équipe du club](https://docs.champagne.dev-ukab.fr/fr/guide/staff/first-day) Vous travaillez au club : vous accueillez les membres, gérez les réservations et répondez aux messages dans le back-office. [Membres](https://docs.champagne.dev-ukab.fr/fr/guide/members/get-started) Vous faites partie d’un club ou voulez en rejoindre un : vous candidatez, réservez et gérez votre adhésion depuis l’espace membre. [Admins du club](https://docs.champagne.dev-ukab.fr/fr/guide/admins/set-up-your-club) Vous dirigez le club : vous configurez ses plans, ses formulaires, son site et ses paiements, puis vous invitez votre équipe. ## Les mots de ce guide [#les-mots-de-ce-guide] | Mot | Sens | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | <span id="back-office">Back-office</span> | Le site que l’équipe du club utilise pour gérer le club. | | <span id="member-app">Espace membre</span> | Le site que les membres utilisent pour candidater, réserver et suivre leur adhésion. | | <span id="plan">Plan</span> | Ce que votre club vend, comme une adhésion mensuelle ou un pack de crédits, avec son prix. | | <span id="credits">Crédits</span> | Un solde que les membres dépensent en réservations et en options. Les membres achètent les crédits par packs. | | <span id="application">Candidature</span> | Le formulaire qu’une personne remplit et signe pour rejoindre un club. L’équipe l’étudie avant qu’elle devienne membre. | | <span id="reservation-category">Catégorie de réservation</span> | Une famille de choses que les membres peuvent réserver, comme des tables, des chambres ou des séances de coaching. | | <span id="role">Rôle</span> | Ce qu’un membre de l’équipe a le droit de voir et de faire dans le back-office. | > Les captures viennent d’un club de démonstration appelé Ukab Clubs. Les personnes, réservations et > montants que vous voyez sont des exemples inventés, saisis en anglais. # Paramètres du club Indiquez l’adresse et le fuseau horaire du club, les champs des membres, les règles de réservation et les catégories à réserver. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/club-settings La section **Paramètres** rassemble les règles qui s’appliquent à tout le club. Elle est organisée en cinq onglets : **Club**, **Membres**, **Réservations**, **Paiements** et **Intégrations**. Chaque carte s’enregistre séparément : changez une valeur, puis sélectionnez le bouton **Enregistrer** de cette carte. Les paramètres s’appliquent au club sélectionné en haut à gauche. Si vous gérez plusieurs clubs, vérifiez son nom avant de modifier quoi que ce soit. ## 1. Indiquer l’adresse et le fuseau horaire du club [#1-indiquer-ladresse-et-le-fuseau-horaire-du-club] Dans le menu de gauche, sélectionnez **Paramètres**. L’onglet **Club** s’ouvre en premier. <figure> <img src="/images/guide/fr/admins/settings-club.png" alt="Onglet Club des paramètres avec une carte Adresse hébergée du club montrant le sous-domaine ukab-clubs, une carte Adresse du club remplie avec 51 rue de la Démonstration, Paris 75008, France, et le début de la carte Fuseau horaire du club." width="1280" height="800" /> <figcaption> L’onglet Club du club d’exemple Ukab Clubs. </figcaption> </figure> | Carte | Ce qu’elle règle | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Adresse hébergée du club** | L’adresse web du site d’inscription de votre club. Voir [Le site de votre club](/fr/guide/admins/hosted-site). | | **Adresse du club** | L’adresse montrée aux membres et aux candidats avec les coordonnées du club. | | **Fuseau horaire du club** | Le fuseau horaire utilisé pour les horaires d’ouverture, les réservations et les rapports. Saisissez une partie du nom d’une ville dans **Fuseau horaire** pour le trouver plus vite. | ## 2. Définir les attributs et les tags des membres [#2-définir-les-attributs-et-les-tags-des-membres] L’onglet **Membres** contient deux cartes. * Les **Attributs des membres** sont des champs supplémentaires conservés sur le profil de chaque membre, comme une langue préférée ou une profession. Les réponses données dans un formulaire de [candidature](/fr/guide#application) peuvent les remplir automatiquement. * Les **Tags des membres** sont des étiquettes réservées à votre équipe, qui servent à regrouper des personnes et à filtrer la liste **Personnes**. Les membres ne les voient jamais. <figure> <img src="/images/guide/fr/admins/settings-members.png" alt="Onglet Membres avec trois attributs des membres, Preferred language (Choix), Printed newsletter (Oui / non) et Profession (Texte), chacun avec des boutons de modification et de suppression, et une carte Tags des membres avec Founding member (3 personnes), Juniors (3 personnes) et Tournament players (4 personnes), chacun avec des boutons de modification et de suppression, et un bouton Ajouter un tag." width="1280" height="800" /> <figcaption> Les trois attributs et les trois tags d’exemple d’Ukab Clubs. Les boutons à utiliser sont entourés en orange. </figcaption> </figure> Pour ajouter un attribut : 1. Sélectionnez **Ajouter un attribut**. 2. Saisissez son **Libellé**, par exemple « Pointure ». Une **Clé** est proposée à partir du libellé. 3. Choisissez son **Type** : **Texte**, **Nombre**, **Date**, **Oui / non** ou **Choix**. Pour un choix, saisissez au moins deux options avec **Ajouter une option**. 4. Sélectionnez **Enregistrer**. Pour ajouter un tag, sélectionnez **Ajouter un tag**, saisissez son **Libellé** et sélectionnez **Enregistrer**. Chaque tag indique combien de personnes le portent. Utilisez le bouton crayon pour renommer un attribut ou un tag, et le bouton corbeille pour le supprimer. > Supprimer un attribut efface la valeur de chaque membre pour cet attribut. Supprimer un tag le > retire de toutes les personnes qui le portent. ## 3. Définir les règles de réservation [#3-définir-les-règles-de-réservation] L’onglet **Réservations** contient les règles qui s’appliquent à toutes les catégories de réservation. | Carte | Ce qu’elle fait | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Invités des réservations** | Activez **Exiger la création d’un compte pour les invités** si les personnes qu’un membre invite doivent avoir un compte. L’interrupteur s’applique dès que vous le basculez. | | **Cautions** | **Rembourser automatiquement après** : les cautions que votre équipe n’a pas remboursées sont rendues en totalité après ce nombre de jours. | | **Sanctions pour absences** | Des sanctions appliquées automatiquement aux membres qui réservent et ne viennent pas. | Pour ajouter une sanction pour absences : 1. Réglez **Remise à zéro du compteur après** et sélectionnez le bouton **Enregistrer** à côté : les absences d’un membre sont oubliées après ce nombre de jours. 2. Sélectionnez **Ajouter une règle**. 3. Saisissez le nombre d’**Absences** qui déclenche la règle. 4. Choisissez la **Sanction** : **Avertir**, **Suspendre**, **Bloquer les réservations** ou **Bannir**. Pour une suspension ou un blocage des réservations, saisissez le nombre de **Jours** qu’il dure. 5. Sélectionnez **Enregistrer**. <figure> <img src="/images/guide/fr/admins/settings-no-show.png" alt="Onglet Réservations avec l’interrupteur Exiger la création d’un compte pour les invités désactivé, Rembourser automatiquement après réglé sur 7 jours, Remise à zéro du compteur après réglé sur 90 jours et une nouvelle règle en cours d’ajout : 3 absences, Suspendre, 7 jours, avec les boutons Annuler et Enregistrer, Enregistrer entouré en orange." width="1280" height="800" /> <figcaption> Une règle d’exemple qui suspend un membre 7 jours après 3 absences. </figcaption> </figure> Chaque règle apparaît ensuite sous forme de carte, comme **3 absences** avec **Suspendu 7 jours**. Ajoutez jusqu’à dix règles pour rendre les sanctions plus sévères à mesure que les absences s’accumulent. Utilisez le bouton crayon pour modifier une règle et le bouton corbeille pour la retirer. ## 4. Préparer ce que les membres peuvent réserver [#4-préparer-ce-que-les-membres-peuvent-réserver] Les [catégories de réservation](/fr/guide#reservation-category), comme un restaurant, une chambre ou un cours collectif, se configurent depuis la section **Réservations**. Chaque élément réservable d’une catégorie, comme une table ou un coach, est une ressource. * Pour créer une catégorie, sélectionnez le bouton **+** au bout de la barre des catégories. * Pour modifier une catégorie, ouvrez-la et sélectionnez **Paramètres de la catégorie**. <figure> <img src="/images/guide/fr/admins/category-settings.png" alt="Fenêtre Paramètres de la catégorie de Le Restaurant sur l’onglet Réservation : Comment les membres réservent réglé sur Un nombre de personnes et un horaire, l’interrupteur Liste d’attente activé, l’interrupteur Non-membres grisé, et les boutons Supprimer la catégorie, Annuler et Enregistrer." width="1280" height="800" /> <figcaption> Les réglages de réservation de la catégorie d’exemple Le Restaurant. </figcaption> </figure> La fenêtre **Paramètres de la catégorie** a cinq onglets : **Général**, **Ressources**, **Réservation**, **Heures** et **Sur place**. Les règles de réservation et les horaires d’ouverture sont expliqués pas à pas dans [Configurer les catégories de réservation](/fr/guide/admins/reservation-categories), les prix et les produits dans [Ressources, prix et produits](/fr/guide/admins/resources-and-prices), et les séances dans [Séances, listes d’invités et périodes bloquées](/fr/guide/admins/sessions-and-blocked-periods). Pour dessiner le plan d’un espace, voir [Éditeur de plan des espaces](/fr/guide/admins/floor-plan-editor). ## 5. Autres paramètres [#5-autres-paramètres] | Où | Carte | À quoi elle sert | | ------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Onglet **Paiements** | **Compte marchand Stripe** | Relie le club aux paiements en ligne. Voir [Paiements](/fr/guide/admins/payments). | | Onglet **Paiements** | **Crédits du club** | Nomme vos crédits et fixe leur valeur. Voir [Paiements](/fr/guide/admins/payments). | | Onglet **Intégrations** | **Envoi des lettres d’information** | Le **Domaine d’envoi**, le **Nom de l’expéditeur** et l’**Adresse d’expédition** de vos lettres d’information. Voir plus bas. | | Onglet **Intégrations** | **Points de terminaison webhook** | Pour votre prestataire technique : envoie les événements du club vers d’autres systèmes. Voir plus bas. | | Onglet **Intégrations** | **Jetons d’API** | Pour votre prestataire technique : permet à un outil comme Terraform de gérer la configuration du club. Seuls les administrateurs le voient. Chaque jeton a ses propres **Permissions** (les domaines auxquels il accède), un niveau d’**Accès**, une **Date d’expiration (facultative)** et, avec **Masquer les données personnelles**, lit les noms des invités anonymisés, et n’est affiché qu’une fois, à sa création. Le menu **⋯** de sa ligne propose **Bloquer**, **Débloquer** et **Supprimer**. | | **Signaler un bug**, en haut à droite | - | Décrivez un problème à l’équipe Champagne : **Titre**, **Que s’est-il passé ?** et **Que faisiez-vous ?**, puis **Envoyer le signalement**. N’indiquez aucun mot de passe. | ### Envoyer les lettres d’information depuis votre propre adresse [#envoyer-les-lettres-dinformation-depuis-votre-propre-adresse] Pour envoyer les lettres d’information depuis votre propre adresse, ouvrez l’onglet **Intégrations**, saisissez un **Domaine d’envoi** réservé aux lettres d’information, un **Nom de l’expéditeur** et une **Adresse d’expédition**, puis sélectionnez **Configurer**. Sélectionnez **Afficher les enregistrements DNS** pour voir les enregistrements à ajouter : transmettez-les à la personne qui gère votre nom de domaine. Une fois qu’ils sont ajoutés, sélectionnez **Vérifier à nouveau**. La carte affiche **En attente du DNS** jusqu’à ce que les enregistrements soient trouvés, puis **Vérifié**. Votre équipe peut alors envoyer des lettres d’information comme décrit dans [Lettres d’information](/fr/guide/staff/newsletters). ### Relier d’autres systèmes avec des webhooks [#relier-dautres-systèmes-avec-des-webhooks] Un webhook permet à Champagne de prévenir un autre système, comme votre propre site web, chaque fois que quelque chose se passe dans votre club. Votre prestataire technique vous donne l’adresse à utiliser. Vous pouvez relier jusqu’à 10 systèmes, chacun avec sa propre adresse, ses événements et sa clé de signature. 1. Dans l’onglet **Intégrations**, sous **Points de terminaison webhook**, sélectionnez **Ajouter un point de terminaison**. Saisissez l’**URL du webhook**, choisissez les **Événements à envoyer** et sélectionnez **Enregistrer**. Le point de terminaison apparaît dans la liste avec sa clé de signature : votre prestataire s’en sert pour vérifier que les messages viennent bien de Champagne. 2. Ouvrez le menu **⋯** d’un point de terminaison et sélectionnez **Afficher la clé** pour la montrer à nouveau et la transmettre à votre prestataire, puis **Masquer la clé**. Chaque affichage est noté dans votre historique d’activité. 3. Dans le même menu, sélectionnez **Envoyer un test**. **Webhook de test envoyé.** signifie que l’autre système l’a reçu. **Webhook de test non accepté** signifie que l’autre système ne l’a pas accepté : le message donne la raison à transmettre à votre prestataire. Le même menu propose aussi **Modifier**, pour changer l’adresse ou les événements, **Mettre en pause**, pour ne plus rien envoyer à ce système jusqu’à ce que vous sélectionniez **Reprendre**, et **Supprimer**. Les événements survenus pendant une pause ne sont pas envoyés ensuite. Si une clé a pu être partagée par erreur, sélectionnez **Réinitialiser la clé** dans le menu de ce point de terminaison et confirmez dans la fenêtre **Réinitialiser la clé de signature ?**. L’ancienne clé cesse aussitôt de fonctionner : donnez la nouvelle à votre prestataire. Les autres points de terminaison gardent leur clé. # Éditeur de plan des espaces Dessinez à la main le plan d’un espace, reliez chaque table ou transat à une ressource réservable et enregistrez la disposition. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/floor-plan-editor Un plan des espaces représente un espace tel qu’il est réellement, comme une plage de piscine avec ses transats ou une salle de restaurant avec ses tables. Votre équipe s’en sert pour voir d’un coup d’œil ce qui est libre et pour réserver une place, comme décrit dans [Utiliser l’agenda du club](/fr/guide/staff/planning). Cette page montre comment construire et modifier le plan à la main. Pour dessiner de nouveaux objets avec l’IA ou reconstruire un plan à partir d’une image, voir [Outils d’IA pour votre plan des espaces](/fr/ai-flows). ## 1. Ouvrir l’éditeur [#1-ouvrir-léditeur] 1. Activez l’interrupteur **Plan des espaces** de la catégorie, dans l’onglet **Ressources** de ses **Paramètres de la catégorie**. Voir [Configurer les catégories de réservation](/fr/guide/admins/reservation-categories). 2. Dans **Réservations**, sélectionnez la catégorie. Le plan apparaît sous le calendrier, en dessous de **Plan des espaces**. 3. Sélectionnez **Modifier le plan**. <figure> <img src="/images/guide/fr/admins/floor-plan-editor.png" alt="Éditeur du plan Piscine extérieure : la Bibliothèque d’objets à gauche avec des formes Réservables comme Rectangle, Cercle et Forme en L, la piscine avec cinq transats, deux plantes et un parasol au milieu, Transat #3 sélectionné, et à droite ses réglages : boutons de calques, Largeur 76, Hauteur 145, Conserver les proportions, Chaises 0, Créer ou attribuer une ressource réglé sur Transat #3, et Modifier la forme. Au-dessus, les boutons Ajouter un plan, Importer un plan, Annuler, Rétablir, de zoom, Abandonner les modifications et Enregistrer le plan." width="1280" height="800" /> <figcaption> Modification du plan de piscine d’exemple de la catégorie Place à la piscine, avec le transat Transat #3 sélectionné. Les numéros renvoient aux trois parties décrites ci-dessous. </figcaption> </figure> L’éditeur a trois parties : 1. la **Bibliothèque d’objets**, à gauche, avec les objets que vous pouvez placer ; 2. le plan lui-même, au milieu ; 3. à droite, les réglages de l’objet sélectionné ou du plan, et **Objets de ce plan**, la liste de tout ce qu’il contient. Rien n’est enregistré tant que vous n’avez pas sélectionné **Enregistrer le plan**. **Abandonner les modifications** efface tout depuis votre dernier enregistrement. Utilisez **Annuler** et **Rétablir** pour revenir en arrière ou en avant. ## 2. Placer des objets [#2-placer-des-objets] La **Bibliothèque d’objets** a deux onglets : * **Réservables** : des formes qui représentent quelque chose que l’on réserve, comme une table, un transat ou une salle : **Rectangle**, **Cercle**, **Triangle**, **Forme en L**, **Hexagone**, **Demi-cercle**, **Forme en U** et **Polygone libre**. * **Décoratif** : des objets de décor, comme des plantes, des portes et d’autres éléments. <figure> <img src="/images/guide/fr/admins/floor-plan-library.png" alt="Bibliothèque d’objets sur l’onglet Réservables avec la forme Rectangle entourée en orange et la forme Cercle à côté, à côté du plan de piscine et de ses Réglages du plan." width="1044" height="268" /> <figcaption> La Bibliothèque d’objets du plan de piscine d’exemple. La commande à utiliser est entourée en orange. </figcaption> </figure> Pour placer un objet, faites-le glisser de la bibliothèque vers le plan. Vous pouvez aussi le sélectionner dans la bibliothèque, puis cliquer à l’endroit voulu sur le plan. Appuyez sur Échap pour annuler. Un plan contient jusqu’à 200 objets. <figure> <video src="/videos/guide/fr/admins/floor-plan-drag.webm" aria-label="La forme Rectangle est glissée de la Bibliothèque d’objets vers le plan de piscine, où elle apparaît sélectionnée avec ses réglages à droite." width="882" height="640" /> <figcaption> Un rectangle glissé sur le plan de piscine d’exemple. </figcaption> </figure> ## 3. Déplacer et disposer les objets [#3-déplacer-et-disposer-les-objets] * Sélectionnez un objet en cliquant dessus sur le plan ou dans **Objets de ce plan**. Son nom et ses réglages apparaissent à droite. * Déplacez-le en le faisant glisser. * Faites-le pivoter avec la poignée de son coin supérieur droit, et redimensionnez-le avec la poignée de son coin inférieur droit. * **Largeur** et **Hauteur** changent la taille d’une forme réservable. Décochez **Conserver les proportions** pour changer l’une sans l’autre. Les objets décoratifs ont un curseur **Échelle**. * Les quatre boutons fléchés règlent l’**Ordre des calques** : **Mettre à l’arrière-plan**, **Reculer**, **Avancer** et **Mettre au premier plan**. Utilisez-les quand des objets se chevauchent. Les boutons à côté du nom de l’objet permettent de **Dupliquer la sélection**, de **Verrouiller ou déverrouiller la sélection** et de **Retirer du plan**. Un objet verrouillé ne peut être ni déplacé ni modifié. Si vous retirez une sélection qui contient des objets verrouillés, Champagne demande **Retirer les objets verrouillés ?** : sélectionnez **Retirer du plan** pour confirmer. | Raccourci | Action | | ----------------------- | ----------------------- | | Suppr ou Retour arrière | Retirer la sélection. | | Échap | Effacer la sélection. | | Ctrl+Z (Cmd+Z sur Mac) | Annuler. | | Ctrl+Maj+Z ou Ctrl+Y | Rétablir. | | Ctrl+D (Cmd+D sur Mac) | Dupliquer la sélection. | Pour regarder de plus près, utilisez les boutons **Zoomer** et **Dézoomer** au-dessus du plan, ou maintenez Maj et tournez la molette de la souris. Pour vous déplacer, maintenez Maj et faites glisser le plan, ou faites glisser avec le bouton du milieu de la souris. Faire glisser une partie vide du plan sans Maj sélectionne tous les objets du cadre que vous tracez. **Réinitialiser la vue** affiche le plan entier à l’écran. ## 4. Relier un objet à une ressource [#4-relier-un-objet-à-une-ressource] Une forme réservable peut être réservée une fois reliée à une ressource de la catégorie. Sélectionnez-la, puis choisissez dans **Créer ou attribuer une ressource** : * une ressource existante, comme Transat #3 ; * **Créer une nouvelle ressource** : une nouvelle ressource est créée à l’enregistrement du plan ; * **Sans réservation** : la forme reste sur le plan sans pouvoir être réservée. Dans une catégorie avec un plan des espaces, c’est ainsi, depuis le plan, que les ressources s’ajoutent et se retirent. <figure> <img src="/images/guide/fr/admins/floor-plan-object.png" alt="Réglages du transat Transat #3 avec 0 Chaises et le menu Créer ou attribuer une ressource réglé sur Transat #3, entouré en orange." width="240" height="172" /> <figcaption> Le transat Transat #3, relié à la ressource du même nom. </figcaption> </figure> ## 5. Ajouter des chaises autour d’une table [#5-ajouter-des-chaises-autour-dune-table] Pour une table, indiquez le nombre de **Chaises** autour de la forme, jusqu’à 16. Les chaises sont placées sur le bord. Pour les placer vous-même, faites glisser une chaise le long du bord de la forme sélectionnée. ## 6. Modifier le contour d’une forme [#6-modifier-le-contour-dune-forme] Pour donner à une forme son contour exact, sélectionnez-la et sélectionnez **Modifier la forme**. <figure> <img src="/images/guide/fr/admins/floor-plan-shape.png" alt="Le même plan de piscine agrandi, avec Transat #3 en mode de modification de forme montrant des points carrés à ses coins, des poignées rondes et des boutons plus sur ses côtés, et Modifier la forme mis en évidence à droite avec l’astuce sur le déplacement des points et l’arrondi des coins." width="1280" height="800" /> <figcaption> Modification de la forme du transat d’exemple Transat #3. La commande à utiliser est entourée en orange. </figcaption> </figure> * Faites glisser un point pour le déplacer. * Sélectionnez un bouton **+** sur un côté pour y ajouter un point. * Tirez une poignée ronde pour arrondir un coin. * Double-cliquez sur un point pour ajouter une courbe. Appuyez sur Suppr pour retirer le point sélectionné. Sélectionnez de nouveau **Modifier la forme** quand vous avez terminé. **Réinitialiser la forme** rétablit le contour d’origine, sans changer la position, la taille ni la rotation. ## 7. Gérer plusieurs plans [#7-gérer-plusieurs-plans] Une catégorie peut avoir plusieurs plans, par exemple un par étage, ou une terrasse et une salle intérieure. Leurs noms s’affichent au-dessus de l’éditeur, avec le nombre d’objets de chacun. * Sélectionnez **Ajouter un plan** pour en créer un, et sélectionnez le nom d’un plan pour l’ouvrir. * Quand rien n’est sélectionné, la partie droite affiche les **Réglages du plan** : le **Nom du plan** et sa **Palette** de couleurs, comme **Lagon**, **Terre cuite**, **Bosquet** ou **Crépuscule**. * **Supprimer le plan** retire un plan et ses objets. Les réservations sont conservées. Le premier plan ne peut pas être supprimé. <figure> <img src="/images/guide/fr/admins/floor-plan-plans.png" alt="Barre des plans avec le plan Piscine extérieure et ses 9 objets, un bouton Ajouter un plan entouré en orange et Importer un plan, au-dessus de l’éditeur, avec les Réglages du plan à droite : Nom du plan, Palette et un bouton Supprimer le plan." width="1044" height="385" /> <figcaption> Le plan unique de la catégorie de piscine d’exemple, avec ses réglages. </figcaption> </figure> ## 8. Enregistrer le plan [#8-enregistrer-le-plan] Sélectionnez **Enregistrer le plan**. Le plan s’affiche alors pour votre équipe dans l’**Agenda du club**, et chaque objet relié indique s’il est libre. Chaque enregistrement est noté dans l’**Historique** du collègue qui l’a fait. Voir [Gérer votre équipe](/fr/guide/admins/manage-your-team). # Le site de votre club Publiez un site d’inscription aux couleurs de votre club, à l’adresse web de votre club. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/hosted-site Votre club peut avoir son propre site de [candidature](/fr/guide#application). Les visiteurs l’ouvrent depuis un lien que vous partagez, par exemple sur votre site web ou vos réseaux sociaux, et demandent à rejoindre votre club. Le site montre le nom de votre club, votre couleur et votre police, avec une petite mention **Propulsé par Champagne**. La mise en place se fait en deux étapes : choisissez une fois l’adresse du site, puis publiez chaque formulaire de candidature que vous voulez y proposer. ## 1. Choisir l’adresse de votre club [#1-choisir-ladresse-de-votre-club] Dans le menu de gauche, sélectionnez **Paramètres**. Dans l’onglet **Club**, trouvez la carte **Adresse hébergée du club**. <figure> <img src="/images/guide/fr/admins/hosted-address.png" alt="Carte Adresse hébergée du club avec le sous-domaine ukab-clubs suivi de l’adresse Champagne, et un bouton Enregistrer." width="1044" height="116" /> <figcaption> L’adresse d’exemple d’Ukab Clubs. Sur votre écran, la fin de l’adresse montre votre domaine Champagne. Le champ à utiliser est entouré en orange. </figcaption> </figure> 1. Dans **Sous-domaine**, saisissez un nom court pour votre club, comme `ukab-clubs`. Les majuscules passent en minuscules, les accents sont retirés, et les espaces et les autres signes deviennent des tirets. 2. Sélectionnez **Enregistrer**. Le site de votre club se trouve alors à ce sous-domaine, suivi de l’adresse Champagne affichée à côté du champ. Cette adresse sert aussi aux conversations sur les réservations. > Si vous changez le sous-domaine, les liens que vous avez déjà partagés avec l’ancienne adresse ne > mènent plus à votre site. Mettez-les à jour partout où vous les avez publiés. ## 2. Publier un formulaire de candidature sur le site [#2-publier-un-formulaire-de-candidature-sur-le-site] Chaque formulaire de candidature, appelé parcours d’inscription, se publie séparément. Dans le menu de gauche, sélectionnez **Candidatures**, puis **Gérer les parcours**, et sélectionnez **Modifier le parcours** sur le formulaire à publier. La construction du formulaire lui-même est expliquée dans [Formulaires de candidature](/fr/guide/admins/onboarding-forms). <figure> <img src="/images/guide/fr/admins/hosted-settings.png" alt="Modification du parcours Founding members : la carte Paramètres du site de candidature a son interrupteur de publication activé, la liste Police ouverte avec Moderne, Classique et Éditorial, une couleur principale #2563EB, et une note indiquant que le formulaire est disponible à l’adresse hébergée du club sur /onboarding/founding-members après l’enregistrement." width="1280" height="800" /> <figcaption> Les paramètres hébergés du parcours d’exemple Founding members, avec la liste des polices ouverte. </figcaption> </figure> Dans la carte **Paramètres du site de candidature** : 1. Activez l’interrupteur en haut à droite de la carte pour publier le formulaire. 2. Choisissez une **Police** : **Moderne**, **Classique** ou **Éditorial**. Chaque option est montrée dans son propre style. 3. Réglez la **Couleur principale** du site. Saisissez un code couleur comme `#2563EB`, ou sélectionnez le bouton pipette pour choisir une couleur. 4. Sélectionnez **Enregistrer le parcours**. Le bas de la carte montre le chemin du formulaire, comme `/onboarding/founding-members`. Ajoutez-le après l’adresse de votre club pour obtenir le lien à partager. Pour retirer un formulaire du site, désactivez l’interrupteur et sélectionnez **Enregistrer le parcours**. ## 3. Ce que voient les visiteurs [#3-ce-que-voient-les-visiteurs] Le lien ouvre une page d’accueil avec le nom et la description du formulaire, dans votre couleur et votre police. <figure> <img src="/images/guide/fr/admins/hosted-entry.png" alt="Site hébergé d’Ukab Clubs en bleu : le formulaire Founding members avec sa description, un bouton Commencer la candidature et un bouton Reprendre une candidature, avec Propulsé par Champagne en haut à droite." width="1280" height="800" /> <figcaption> Le formulaire d’exemple Founding members, tel que les visiteurs le voient sur le site d’Ukab Clubs. </figcaption> </figure> * **Commencer la candidature** ouvre le formulaire, étape par étape. * **Reprendre une candidature** permet à quelqu’un qui a déjà commencé d’y revenir : la personne saisit son adresse e-mail et reçoit un code. <figure> <img src="/images/guide/fr/admins/hosted-application.png" alt="Première étape de la candidature pour rejoindre Ukab Clubs sur le site hébergé, étape 1 sur 4, avec les champs Prénom et Nom et un bouton Continuer." width="1280" height="800" /> <figcaption> La première étape de la candidature d’exemple sur le site d’Ukab Clubs. </figcaption> </figure> <figure> <img src="/images/guide/fr/admins/hosted-continue.png" alt="Carte Retrouver ma candidature sur le site hébergé avec un champ Adresse e-mail et un bouton Envoyer le code." width="448" height="334" /> <figcaption> La page derrière Reprendre une candidature. </figcaption> </figure> Les e-mails sur la candidature, ainsi que les pages de paiement et de signature, ramènent les candidats sur le site de votre club. Une fois toutes les étapes après acceptation terminées, les membres se connectent à leur compte sur l’[espace membre](/fr/guide#member-app) Champagne : le site leur montre un lien **Me connecter à mon compte**. ### Quand le site affiche « Site de candidature indisponible » [#quand-le-site-affiche--site-de-candidature-indisponible-] Les visiteurs voient cette page quand le lien ne mène pas à un formulaire publié de votre club. <figure> <img src="/images/guide/fr/admins/hosted-unavailable.png" alt="Page blanche avec le titre Site de candidature indisponible et la ligne Vérifiez l’adresse ou contactez le club." width="576" height="144" /> <figcaption> La page affichée pour un lien d’exemple vers un formulaire qui n’existe pas. </figcaption> </figure> Vérifiez trois choses : * le lien utilise le sous-domaine actuel de votre club, tel que réglé dans **Adresse hébergée du club** ; * le chemin après `/onboarding/` correspond à celui affiché en bas de la carte **Paramètres du site de candidature** du formulaire ; * l’interrupteur de publication du formulaire est activé, et le formulaire a été enregistré. Les nouvelles candidatures arrivent dans **Candidatures**, comme toutes les autres. Voir [Étudier les candidatures](/fr/guide/staff/applications). # Inviter votre équipe Donnez à un collègue l’accès au back-office et choisissez ce qu’il peut voir et faire. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/invite-your-team Chaque personne de votre équipe se connecte avec son propre compte d’équipe. Ce compte est créé quand vous lui envoyez une invitation depuis la section **Équipe**. Vous choisissez pour quels clubs elle travaille et ce qu’elle a le droit d’y faire. ## 1. Ouvrir la section Équipe [#1-ouvrir-la-section-équipe] Dans le menu de gauche, sélectionnez **Équipe**. La liste affiche toutes les personnes qui ont accès à vos clubs : leurs **Clubs**, leur **Rôle** et les catégories du **Planning** qu’elles peuvent voir. <figure> <img src="/images/guide/fr/admins/team.png" alt="Section Équipe listant les membres de l’équipe avec leur e-mail, leurs clubs, leur rôle (Propriétaire, Administrateur ou Équipe) et leur accès au planning, avec les boutons Inviter un membre de l’équipe et Inviter un administrateur." width="1280" height="800" /> <figcaption> L’équipe d’exemple d’Ukab Clubs et de ses clubs associés. </figcaption> </figure> Il existe trois [rôles](/fr/guide#role) : | Rôle | Ce qu’il permet | | ------------------ | ---------------------------------------------------------------------------- | | **Propriétaire** | Le compte à qui appartiennent vos clubs. Accès complet. | | **Administrateur** | Tout, dans les clubs qui lui sont attribués. | | **Équipe** | Seulement les tâches que vous cochez, dans les clubs qui lui sont attribués. | ## 2. Inviter un membre de l’équipe [#2-inviter-un-membre-de-léquipe] Sélectionnez **Inviter un membre de l’équipe**. 1. Saisissez son adresse dans **E-mail**. 2. Dans **Accès aux clubs**, cochez chaque club pour lequel il travaille. 3. Pour chaque club, cochez ce qu’il peut faire. Les autorisations sont regroupées par thème : **Membres et candidatures**, **Activités quotidiennes**, **Finances** et **Configuration du club**. 4. Dans **Catégories du planning accessibles**, cochez les activités dont il doit voir le planning, comme les cours particuliers pour un coach. 5. Sélectionnez **Envoyer l’invitation**. <figure> <img src="/images/guide/fr/admins/invite-staff.png" alt="Fenêtre Inviter un membre de l’équipe avec un e-mail d’exemple, Ukab Clubs coché dans Accès aux clubs et trois autorisations sélectionnées, dont Consulter les fiches membres et Consulter le planning attribué." width="1280" height="800" /> <figcaption> Invitation d’un coach d’exemple qui peut consulter les fiches des membres, voir son planning et régler ses propres disponibilités. </figcaption> </figure> > Donnez seulement les autorisations dont la personne a besoin aujourd’hui. Vous pouvez en ajouter à > tout moment en ouvrant sa ligne dans **Équipe**, et les changements s’appliquent dès que vous > enregistrez. [Qui voit quoi](/fr/guide/admins/who-sees-what) indique ce que chaque autorisation > affiche. Pour donner un accès complet à quelqu’un, sélectionnez plutôt **Inviter un administrateur** et cochez ses clubs. ## 3. Ce que reçoit votre collègue [#3-ce-que-reçoit-votre-collègue] Votre collègue reçoit un e-mail avec un lien d’invitation. Le lien ouvre une page **Rejoindre le back-office** où il saisit son nom, choisit un mot de passe et sélectionne **Accepter l’invitation**. Il peut ensuite [se connecter](/fr/guide/staff/getting-started). <figure> <img src="/images/guide/fr/admins/join-back-office.png" alt="Page Rejoindre le back-office avec les champs Votre nom et Mot de passe, la ligne Déjà un compte ? Utilisez votre mot de passe actuel., un bouton Accepter l’invitation entouré en orange et un lien Retour à la connexion." width="448" height="388" /> <figcaption> La page qu’ouvre un lien d’invitation. </figcaption> </figure> Tant qu’il n’a pas accepté, sa ligne dans **Équipe** affiche **Invitation en attente**. Pour retirer une invitation, sélectionnez l’icône d’enveloppe barrée d’une croix à côté de cette mention : son info-bulle indique **Annuler l’invitation**. Une invitation jamais acceptée finit par afficher **Invitation expirée** : envoyez-en une nouvelle. Pour modifier plus tard l’accès d’un collègue, ajouter ses documents professionnels ou le retirer de votre équipe, consultez [Gérer votre équipe](/fr/guide/admins/manage-your-team). # Gérer votre équipe Trouvez un collègue, modifiez ce qu’il peut faire, suivez son activité et retirez son accès. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/manage-your-team Une fois qu’un collègue a rejoint le club, tout ce qui le concerne est rangé dans sa fiche d’équipe, dans la section **Équipe** : le profil que voient les membres, ses diplômes, ses accès, ses horaires, ses réservations et un journal de ce qu’il a fait dans le [back-office](/fr/guide#back-office). Pour ajouter quelqu’un à votre équipe, voir [Inviter votre équipe](/fr/guide/admins/invite-your-team). ## 1. Trouver un collègue [#1-trouver-un-collègue] Dans le menu de gauche, sélectionnez **Équipe**. Saisissez une partie d’un nom ou d’une adresse e-mail dans **Rechercher par nom ou e-mail…** pour raccourcir la liste. Pour affiner encore la liste, sélectionnez **Filtres**, choisissez vos filtres et sélectionnez **Appliquer les filtres**. **Réinitialiser** les efface. <figure> <img src="/images/guide/fr/admins/team-filters.png" alt="Section Équipe avec la fenêtre Filtres ouverte au-dessus de la liste : Rôle réglé sur Tous les rôles, Statut réglé sur Tous les statuts, Justificatifs professionnels réglé sur Documents expirés, et les boutons Réinitialiser, Annuler et Appliquer les filtres." width="1280" height="800" /> <figcaption> La recherche des documents expirés dans l’équipe d’exemple d’Ukab Clubs. La commande à utiliser est entourée en orange. </figcaption> </figure> | Filtre | Choix | | -------------------------------- | --------------------------------------------------------------------------------------- | | **Rôle** | **Tous les rôles**, **Administrateurs** ou **Équipe**. | | **Statut** | **Tous les statuts**, **Inscrit**, **Invitation en attente** ou **Invitation expirée**. | | **Justificatifs professionnels** | **Tous les documents**, **Documents expirés** ou **Expire sous 30 jours**. | Quand un collègue a un document expiré ou qui expire dans les 30 jours, sa ligne l’indique, par exemple **1 expiré** ou **Expire dans 12 j**. Les documents expirés s’affichent en premier. <figure> <img src="/images/guide/fr/admins/team-expired-documents.png" alt="Liste Équipe filtrée sur une ligne, Sport coach avec ukab.staff@example.com, une mention rouge 1 expiré, le club Ukab Clubs, le rôle Équipe et sept catégories du planning, et le bouton Filtres (1) en haut à droite." width="1280" height="800" /> <figcaption> Avec le filtre Documents expirés, seul le coach d’exemple Sport coach reste affiché. </figcaption> </figure> Sélectionnez la ligne d’un collègue pour ouvrir sa fiche. Elle a cinq onglets : **Profil**, **Accès**, **Disponibilité**, **Réservations** et **Historique**. Pour modifier son profil et ses documents professionnels, voir [Profils et documents de votre équipe](/fr/guide/admins/team-documents). ## 2. Modifier ce qu’il peut faire [#2-modifier-ce-quil-peut-faire] Ouvrez l’onglet **Accès**. Il présente les mêmes choix que l’invitation : **Accès aux clubs**, puis, pour chaque club, les autorisations regroupées par thème et les **Catégories du planning accessibles**. <figure> <img src="/images/guide/fr/admins/team-access.png" alt="Onglet Accès de Sport coach : Ukab Clubs coché dans Accès aux clubs, la section Ukab Clubs résumée par 7 permissions sélectionnées et 7 catégories assignées, Consulter les fiches membres marqué Inclus automatiquement, et Consulter le planning attribué, Créer et modifier les réservations, Valider les présences et les billets et Gérer ses propres disponibilités cochés, avec les boutons Retirer de l’espace, Annuler et Enregistrer les modifications." width="1280" height="800" /> <figcaption> Les accès du coach d’exemple Sport coach à Ukab Clubs. </figcaption> </figure> * Le titre de chaque club résume ses accès, par exemple **7 permissions sélectionnées · 7 catégories assignées**. **Modifié** apparaît à côté d’un club que vous avez changé. * **Inclus automatiquement** signale une autorisation qui vient avec une autre que vous avez cochée. Par exemple, **Modifier les notes internes** inclut **Consulter les fiches membres**. * Si la personne peut voir le planning sans aucune catégorie, le club affiche **Planning sans catégorie assignée** : cochez au moins une catégorie. Sélectionnez **Enregistrer les modifications**. Les changements s’appliquent immédiatement. L’onglet **Accès** n’est pas disponible pour le propriétaire de vos clubs, qui a toujours un accès complet. ## 3. Voir ses horaires, ses réservations et son historique [#3-voir-ses-horaires-ses-réservations-et-son-historique] * **Disponibilité** montre ses heures de travail semaine par semaine, comme décrit dans [Vos réservations et vos disponibilités](/fr/guide/staff/schedule-and-availability). Chaque collègue modifie ses propres heures depuis **Mes disponibilités**. * **Réservations** liste les réservations faites avec lui, par exemple ses cours. * **Historique** affiche l’**Historique d’activité** : ce que ce collègue a fait dans le back-office, comme **A créé une réservation**, **A envoyé une lettre d’information** ou **A modifié les accès d’un membre de l’équipe**, avec la personne concernée, la date et le club. Quand une ligne a une flèche, sélectionnez-la pour voir le détail du changement. Un collègue qui n’a encore rien fait affiche **Aucune action enregistrée.** <figure> <img src="/images/guide/fr/admins/team-history.png" alt="Onglet Historique de Sport coach avec deux lignes : A déclaré une consommation de produits pour Jamie Chen hier à Ukab Clubs, ouverte pour montrer les Produits passés à Sparkling water × 2, Champagne bottle × 1, et A enregistré le départ d’une réservation pour Sam Rivera il y a trois jours." width="1280" height="800" /> <figcaption> L’historique du coach d’exemple, avec les produits du minibar déclarés pour le séjour de Jamie Chen. </figcaption> </figure> Quand un collègue travaille pour plusieurs clubs, choisissez le **Club** en haut des onglets **Disponibilité** et **Réservations**. ## 4. Retirer un collègue [#4-retirer-un-collègue] 1. Ouvrez sa fiche, puis l’onglet **Accès**. 2. Sélectionnez **Retirer de l’espace**. 3. Lisez le message, puis sélectionnez **Retirer l’accès** pour confirmer, ou **Conserver l’accès** pour revenir en arrière. Votre collègue perd immédiatement l’accès à tous les clubs de votre espace. Pour le retirer d’un seul club, décochez ce club dans **Accès aux clubs** et sélectionnez **Enregistrer les modifications**. # Formulaires de candidature Construisez les formulaires que les personnes remplissent pour rejoindre votre club, et choisissez ce qu’elles terminent une fois acceptées. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/onboarding-forms Les personnes rejoignent votre club en remplissant un formulaire de [candidature](/fr/guide#application). Dans Champagne, chaque formulaire s’appelle un parcours d’inscription. Vous pouvez en avoir plusieurs, par exemple un pour les membres habituels et un pour les membres fondateurs, chacun avec ses questions et ses [plans](/fr/guide#plan). Vous les configurez depuis la section **Candidatures**. Pour lire les candidatures reçues et prendre une décision, voir [Étudier les candidatures](/fr/guide/staff/applications). ## 1. Ouvrir vos formulaires [#1-ouvrir-vos-formulaires] Dans le menu de gauche, sélectionnez **Candidatures**, puis **Gérer les parcours**. Chaque carte montre le nom et la description d’un formulaire, avec son **Identifiant du parcours**, le code qui l’identifie. Sélectionnez **Copier l’identifiant** pour le copier, par exemple pour le développeur de votre site web. <figure> <img src="/images/guide/fr/admins/onboarding-list.png" alt="Page des parcours d’inscription avec deux cartes, Common members et Founding members, chacune avec sa description, son Identifiant du parcours, un bouton Copier l’identifiant et un bouton Modifier le parcours, le premier entouré en orange, et un bouton Ajouter un parcours en haut à droite." width="1280" height="800" /> <figcaption> Les deux formulaires d’exemple d’Ukab Clubs. </figcaption> </figure> * Pour modifier un formulaire, sélectionnez **Modifier le parcours** sur sa carte. * Pour en créer un, sélectionnez **Ajouter un parcours**, saisissez un **Nom** que votre équipe reconnaîtra et une **Description** de qui doit l’utiliser, puis sélectionnez **Créer un parcours**. Le nouveau formulaire s’ouvre. **Retour aux parcours**, en haut d’un formulaire, vous ramène à la liste. Si un formulaire affiche **Parcours ou club indisponible**, il a peut-être été supprimé ou appartient à un autre club : vérifiez le club sélectionné en haut à gauche. ## 2. Nommer le formulaire et sa page hébergée [#2-nommer-le-formulaire-et-sa-page-hébergée] Le haut du formulaire contient deux cartes. * **Paramètres du parcours** : le **Nom** et la **Description** que voient les candidats quand ils choisissent comment rejoindre le club. * **Paramètres du site de candidature** : Champagne peut publier le formulaire sur une page prête à l’emploi, aux couleurs du club. Activez l’interrupteur, puis choisissez une **Police** (**Moderne**, **Classique** ou **Éditorial**) et une **Couleur principale**. La carte donne l’adresse de la page, qui est en ligne dès que vous enregistrez. <figure> <img src="/images/guide/fr/admins/onboarding-settings.png" alt="Page de modification du parcours Common members avec un bouton Enregistrer le parcours, la carte Paramètres du parcours avec son nom, sa description et son identifiant, et la carte Paramètres du site de candidature activée avec la police Moderne et une couleur principale bleue." width="1280" height="800" /> <figcaption> Le formulaire d’exemple Common members d’Ukab Clubs, publié sur une page hébergée. </figcaption> </figure> Pour configurer le reste du site de votre club, voir [Le site de votre club](/fr/guide/admins/hosted-site). ## 3. Construire les questions [#3-construire-les-questions] Sous les paramètres, le formulaire est une liste de blocs, montrés aux candidats dans cet ordre. Utilisez les flèches d’un bloc pour le monter ou le descendre, et le bouton corbeille pour le retirer. Pour ajouter un bloc, utilisez les boutons juste sous les blocs, au-dessus de la carte **Après acceptation**. Un formulaire contient jusqu’à 50 blocs. | Bloc | Ce que fait le candidat | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **Nom du candidat** | Il saisit son prénom et son nom. Présent dans chaque formulaire. | | **Vérification de l’e-mail** | Il saisit son adresse e-mail, puis un code envoyé à cette adresse. Présent dans chaque formulaire. | | **Question** | Il répond à une question que vous rédigez. | | **Adresse du candidat** | Il saisit son adresse postale. Elle est copiée sur son profil. | | **Choix du plan** | Il choisit l’un des plans d’adhésion que vous cochez dans le bloc. | | **Vérification du téléphone** | Il saisit son numéro de mobile, puis un code reçu par SMS. Il est copié sur son profil. Toujours obligatoire. | | **Vérification d’identité** | Il fait vérifier une pièce d’identité par Stripe Identity, comme décrit à l’[étape 4](#4-choisir-les-étapes-après-acceptation). | | **Réservation de visite** | Il réserve une visite gratuite, comme décrit à l’[étape 4](#4-choisir-les-étapes-après-acceptation). | | **Parrainage** | Il saisit les codes de parrainage que des membres du club lui ont donnés. Vous réglez le **Nombre de parrains**. | **Adresse du candidat**, **Choix du plan**, **Vérification du téléphone**, **Vérification d’identité**, **Réservation de visite** et **Parrainage** ne peuvent être ajoutés qu’une fois chacun. **Vérification d’identité** n’apparaît qu’une fois Stripe Identity configuré pour Champagne. Placez **Vérification d’identité** et **Réservation de visite** sous **Nom du candidat** et **Vérification de l’e-mail** : les flèches ne permettent pas de les remonter au-dessus. **Adresse du candidat**, **Vérification du téléphone**, **Vérification d’identité** et **Réservation de visite** peuvent être demandés ici, avant acceptation, ou dans la carte **Après acceptation**, mais pas les deux : un bloc déjà ajouté ici est grisé dans la carte avec **Déjà demandé avant acceptation.**, et inversement. Une visite réservée avant acceptation est rattachée au profil du nouveau membre quand vous acceptez la candidature, et annulée si vous la refusez. Dans un bloc **Choix du plan**, cochez les plans que les candidats peuvent choisir sous **Plans inclus dans le parcours**. Les autres plans restent disponibles pour les membres une fois qu’ils ont rejoint le club. Si votre club n’a encore aucun plan, le bloc affiche **Aucun plan disponible pour ce club. Ajoutez d’abord un plan.** : voir [Plans et tarifs](/fr/guide/admins/plans-and-pricing). Pour chaque **Question** : 1. Saisissez son **Titre**, par exemple « Pourquoi souhaitez-vous nous rejoindre ? ». 2. Choisissez son **Type** : **Réponse courte** pour une ligne de texte, **Réponse longue** pour un paragraphe, ou **Options** pour un choix entre 2 et 20 réponses que vous saisissez. 3. Dans **Enregistrer dans**, choisissez où va la réponse. **Candidature uniquement** la garde avec la candidature. Un champ des **Champs de la personne**, comme **Téléphone** ou **Date de naissance**, ou l’un de vos **Attributs des membres** copie la réponse sur le profil du nouveau membre. Chaque champ ne peut recevoir qu’une seule question. 4. Cochez **Obligatoire** si le candidat doit répondre. <figure> <img src="/images/guide/fr/admins/onboarding-questions.png" alt="Trois blocs de question : Why would you like to join? en réponse longue obligatoire, Your phone number en réponse courte enregistrée dans Téléphone, et Which language do you prefer? en options enregistrées dans Preferred language, avec la liste Enregistrer dans ouverte sur Candidature uniquement, Champs de la personne et Attributs des membres." width="1280" height="800" /> <figcaption> Les questions du formulaire d’exemple Common members, avec la liste Enregistrer dans ouverte. La commande à utiliser est entourée en orange. </figcaption> </figure> Quand une question enregistre sa réponse dans un champ aux réponses fixes, comme **Genre**, le candidat choisit parmi les réponses de ce champ, listées sous la question. ## 4. Choisir les étapes après acceptation [#4-choisir-les-étapes-après-acceptation] La carte **Après acceptation** liste ce que les candidats acceptés doivent encore terminer, depuis le lien de leur e-mail d’acceptation. * **Encaisser le paiement du plan** : le candidat paie le plan qu’il a choisi. Il faut pour cela un bloc **Choix du plan** dans le formulaire : sans lui, l’option est grisée et marquée **Choix du plan obligatoire**. * **Exiger la signature d’un document** : le candidat signe un PDF en ligne, comme le règlement de votre club. Sélectionnez **Importer un PDF** pour ajouter le document, jusqu’à 2 Mo. Si vous essayez d’enregistrer sans document, l’option affiche **Document obligatoire.** Si le fichier est refusé, un message **Échec de l’envoi du document** en donne la raison, par exemple un fichier qui n’est pas un PDF valide ou qui est trop lourd. Choisissez un autre fichier et réessayez. * **Recueillir l’adresse du candidat** : le candidat saisit son adresse postale, enregistrée aussitôt sur son profil. * **Vérifier l’identité du candidat** : le candidat photographie sa carte d’identité, son passeport ou son permis de conduire avec Stripe Identity, qui vérifie que le document est authentique et non expiré. Réglez les contrôles sous l’option : * **Âge minimum** : comparé à la date de naissance du document, 18 par défaut. Saisissez 0 pour ne pas contrôler l’âge. * **Exiger un selfie** : Stripe compare le visage du candidat avec la photo du document. * **Exiger que le nom corresponde** : le nom du document doit contenir le prénom et le nom indiqués dans la candidature. Les seconds prénoms du document sont acceptés. L’option n’apparaît qu’une fois Stripe Identity configuré pour Champagne. Si un formulaire contient déjà cette étape et que Stripe Identity n’est plus disponible, l’option l’indique : décochez-la pour enregistrer le formulaire. Champagne ne conserve que le résultat de chaque contrôle et la date de naissance, jamais les photos ni le numéro du document. * **Vérifier le téléphone du candidat** : le candidat confirme un numéro de mobile avec un code reçu par SMS, enregistré aussitôt sur son profil. * **Réserver une visite** : le candidat choisit un horaire dans une catégorie de réservation, par exemple une visite découverte, et peut le modifier ou l’annuler tant qu’il n’a pas continué. Choisissez la **Catégorie de réservation** : seules les catégories réservées par créneau sont listées, et celles avec des tarifs ne peuvent pas être choisies, car le parcours n’encaisse jamais de paiement. Désactivez **Obligatoire** pour permettre aux candidats de passer l’étape. Activez **Réservable uniquement dans un parcours** dans les [options de la catégorie](/fr/guide/admins/reservation-categories) pour la masquer aux membres. La réservation suit les horaires d’ouverture, le délai et la capacité de la catégorie, apparaît dans le planning, et la candidature affiche la visite réservée avec un lien vers la réservation. Les candidats acceptés terminent ces étapes dans cet ordre : paiement, signature, adresse, identité, téléphone, puis visite. * **URL de reprise pour le candidat** : laissez ce champ vide pour garder ces étapes sur Champagne. Remplissez-le seulement si votre propre site web prend le relais après l’acceptation. <figure> <img src="/images/guide/fr/admins/onboarding-after-approval.png" alt="Les boutons d’ajout de bloc, de Question à Vérification de l’e-mail avec 7 / 50 blocs, puis la carte Après acceptation avec Encaisser le paiement du plan et Exiger la signature d’un document cochés, le document club-rules.pdf, un champ URL de reprise pour le candidat vide, et le bouton Enregistrer le parcours entouré en orange." width="1280" height="800" /> <figcaption> Le formulaire d’exemple demande aux candidats acceptés de payer leur plan et de signer le règlement du club. </figcaption> </figure> ## 5. Enregistrer le formulaire [#5-enregistrer-le-formulaire] Sélectionnez **Enregistrer le parcours**. Jusque-là, la page affiche **Modifications non enregistrées** à côté du nom du formulaire, et Champagne vous demande de confirmer si vous quittez la page. L’enregistrement vous ramène à vos formulaires. > Si un collègue enregistre le même formulaire pendant que vous le modifiez, une fenêtre **Ce > parcours d’inscription a été modifié** s’ouvre. Sélectionnez **Recharger** pour charger sa > version, ou **Continuer la modification** pour poursuivre avec la vôtre : l’enregistrement > remplace alors sa version. ## Ce que voient les candidats [#ce-que-voient-les-candidats] Les candidats choisissent un formulaire sur la page de votre club et le remplissent une étape à la fois. Voir [Trouver un club et candidater](/fr/guide/members/find-a-club-and-apply) et [Remplir et signer votre candidature](/fr/guide/members/sign-your-application). Les plans qu’ils peuvent choisir viennent de [Plans et tarifs](/fr/guide/admins/plans-and-pricing). # Paiements Reliez votre club à Stripe pour que les membres puissent payer en ligne, et configurez les crédits de votre club. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/payments Les membres paient votre club en ligne par carte via Stripe, un service de paiement en ligne. L’argent va sur le compte Stripe de votre club, appelé compte marchand dans Champagne. Une fois votre club relié à un compte marchand, les membres peuvent payer leur adhésion, recharger leurs [crédits](/fr/guide#credits) et régler leurs factures depuis leur compte. Vous configurez cela dans **Paramètres**, dans l’onglet **Paiements**. ## 1. Vérifier le compte marchand de votre club [#1-vérifier-le-compte-marchand-de-votre-club] Dans le menu de gauche, sélectionnez **Paramètres**, puis l’onglet **Paiements**. La carte **Compte marchand Stripe** affiche **Configuré** quand votre club est relié, et **Non configuré** sinon. <figure> <img src="/images/guide/fr/admins/payments-merchant.png" alt="Onglet Paiements avec la carte Compte marchand Stripe marquée Configuré : le compte marchand Ukab Clubs sélectionné pour ce club, ses informations avec le pays France et le type d’entité Particulier, une liste Disponibilité du paiement où Paiements, Virements et Catalogue sont Activé ou Prêt, et un bouton Vérifier le catalogue." width="1280" height="800" /> <figcaption> Le compte marchand d’exemple d’Ukab Clubs, prêt à recevoir des paiements. Son e-mail est flouté. </figcaption> </figure> **Disponibilité du paiement** vous indique ce qui fonctionne aujourd’hui : | Ligne | Prêt quand elle affiche | Signification | | ------------- | ----------------------- | -------------------------------------------------------------------- | | **Paiements** | **Activé** | Stripe accepte les paiements par carte pour votre club. | | **Virements** | **Activé** | Stripe peut virer l’argent encaissé sur votre compte bancaire. | | **Catalogue** | **Prêt** | Vos plans sont préparés sur Stripe, les membres peuvent les acheter. | Quand une ligne n’est pas prête, elle affiche **Non activé** pour **Paiements** et **Virements**, ou **À préparer** pour **Catalogue**. La section 2 explique quoi faire. L’état se met à jour tout seul tant que la page est ouverte. **Dernière vérification** indique quand il a été actualisé pour la dernière fois. D’autres messages peuvent apparaître sur la carte : | Message | Signification | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | **Recherche de mises à jour…** | Champagne demande à Stripe le dernier état. | | **Mises à jour automatiques interrompues** | Champagne n’a pas pu obtenir le dernier état. La carte montre le dernier connu. Rechargez la page pour réessayer. | | **Pas encore vérifié** | Affiché après **Dernière vérification** quand Champagne n’a encore reçu aucun état de Stripe. | | **Mise à jour du compte marchand…** | Un changement que vous avez demandé, comme **Préparer le catalogue**, est en cours. Attendez qu’il se termine. | ## 2. Relier un club à Stripe [#2-relier-un-club-à-stripe] Si la carte affiche **Non configuré**, vous avez deux possibilités. * Utiliser un compte marchand existant. Les comptes marchands que vous avez déjà créés pour vos autres clubs sont listés avec leurs informations. Sélectionnez **Utiliser ce compte marchand** sur le bon : les deux clubs encaissent alors les paiements sur le même compte Stripe. * Créer un nouveau compte marchand. Sélectionnez **Créer un compte marchand** et remplissez le formulaire. <figure> <img src="/images/guide/fr/admins/payments-choose-merchant.png" alt="Carte Compte marchand Stripe marquée Non configuré pour le club Vosges : le compte marchand existant Ukab Clubs avec un bouton Utiliser ce compte marchand, puis un formulaire de nouveau compte marchand rempli avec le nom Vosges et l’e-mail billing@example.com, le pays FR, une forme juridique vide, le type d’entité Entreprise, et les boutons Créer un compte marchand et Annuler." width="1044" height="782" /> <figcaption> La connexion du club d’exemple Vosges : réutiliser le compte marchand Ukab Clubs ou en créer un nouveau. La commande à utiliser est entourée en orange. </figcaption> </figure> Pour créer un compte marchand : 1. Saisissez le **Nom du compte marchand**, en général le nom de votre club ou de votre société, et un **E-mail** de contact. 2. Saisissez le **Pays (code ISO)**, le code à deux lettres de votre pays, comme **FR** pour la France. 3. Si vous le souhaitez, saisissez la **Forme juridique**, comme SAS, SARL ou association. 4. Choisissez le **Type d’entité** : **Entreprise**, **Particulier**, **Association à but non lucratif** ou **Organisme public**. 5. Sélectionnez **Créer un compte marchand**. Le nouveau compte marchand affiche alors **Configuration à terminer**. Sélectionnez **Terminer l’activation Stripe** : Stripe s’ouvre dans un nouvel onglet et demande les informations de votre société et votre compte bancaire. Une fois ces étapes terminées, revenez dans Champagne. La carte se met à jour toute seule. > Si Stripe ne s’ouvre pas, autorisez les fenêtres pop-up pour Champagne dans votre navigateur et > sélectionnez de nouveau **Terminer l’activation Stripe**. Quand **Catalogue** affiche **À préparer**, sélectionnez **Préparer le catalogue** pour publier vos [plans](/fr/guide#plan) sur Stripe. Plus tard, **Vérifier le catalogue** contrôle qu’ils sont toujours en place. ## 3. Nommer les crédits de votre club [#3-nommer-les-crédits-de-votre-club] Les crédits sont une monnaie propre à votre club, que les membres achètent par packs et dépensent en réservations. La carte **Crédits du club**, dans le même onglet, les configure. <figure> <img src="/images/guide/fr/admins/payments-credits.png" alt="Carte Crédits du club avec le nom de crédits Stars, 10 crédits pour 1 €, une alerte de solde bas à 10,00 €, un lien Gérer les recharges et un bouton Enregistrer." width="1044" height="324" /> <figcaption> Les crédits d’exemple d’Ukab Clubs, appelés Stars. </figcaption> </figure> 1. Saisissez le **Nom des crédits** que verront les membres, par exemple « Stars ». 2. Dans **Crédits pour 1 €**, réglez la valeur de référence d’un crédit. Elle sert à montrer combien les membres économisent avec chaque pack. La changer ne modifie ni les soldes ni les packs existants. 3. Dans **Alerte de solde bas**, saisissez le montant sous lequel les membres reçoivent un e-mail. Saisissez 0 pour désactiver l’alerte. 4. Sélectionnez **Enregistrer**. Pour créer les packs que les membres achètent, sélectionnez **Gérer les recharges**. Voir [Plans et tarifs](/fr/guide/admins/plans-and-pricing). ## 4. Ce que les membres paient en ligne [#4-ce-que-les-membres-paient-en-ligne] Une fois votre compte marchand prêt, les membres paient par carte sur une page Stripe sécurisée : | Quoi | Où les membres le paient | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Adhésion | Quand ils choisissent un plan, puis à chaque renouvellement. Voir [Votre adhésion](/fr/guide/members/membership-and-credits). | | Packs de crédits | Avec **Recharger pour …** dans leurs crédits. | | Factures | Avec **Payer sur Stripe** dans leurs factures. Voir [Facturation](/fr/guide/members/billing). | | Réservations des non-membres | Par carte, pour les catégories ouvertes aux non-membres. | Les membres paient leurs réservations en crédits quand une ressource a un prix en crédits. Les prix et les cautions de chaque ressource se règlent dans ses propres réglages : voir [Ressources, prix et produits](/fr/guide/admins/resources-and-prices#2-d%C3%A9finir-les-r%C3%A8gles-de-la-ressource). Pour suivre ce qui a été payé, voir [Ventes et factures](/fr/guide/staff/sales-and-invoices). # Plans et tarifs Créez des plans d’adhésion, des packs de crédits et des options, et changez le prix d’un plan. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/plans-and-pricing La section **Plans** liste tout ce que votre club vend en ligne : les adhésions, les packs de [crédits](/fr/guide#credits) et les options. Les membres paient les adhésions et les packs de crédits par carte via Stripe, le service de paiement en ligne relié à votre club, et les options avec leurs crédits. > Si votre club n’a pas encore de compte Stripe, la page affiche **Connectez un compte marchand > Stripe avant d’ajouter des plans** et **Ajouter un plan** reste grisé. Sélectionnez **Ouvrir les > paramètres de paiement**, puis suivez [Paiements](/fr/guide/admins/payments). ## 1. Voir vos plans [#1-voir-vos-plans] Dans le menu de gauche, sélectionnez **Plans**. Chaque ligne montre le nom et la description du [plan](/fr/guide#plan) (les crédits inclus, pour un pack de crédits), son **Type**, son **Montant** et sa **Périodicité**. Sélectionnez le titre d’une colonne pour trier la liste. Un club sans plan affiche **Aucun plan pour le moment.** Si la liste affiche **Plans indisponibles**, vos plans n’ont pas pu être chargés : rechargez la page. <figure> <img src="/images/guide/fr/admins/plans.png" alt="Section Plans avec un bouton Ajouter un plan et six plans : trois abonnements annuels à 1 800 €, 4 500 € et 6 300 €, et trois packs de crédits ponctuels, Discovery avec 250 Stars pour 25 €, Regular avec 1 250 Stars pour 100 € et Weekend avec 600 Stars pour 50 €." width="1280" height="800" /> <figcaption> Les plans d’exemple d’Ukab Clubs : trois adhésions annuelles et trois packs de crédits. </figcaption> </figure> Il existe trois types de plan : | Type | Ce que le membre achète | Prix | | ------------------- | ----------------------------------------------------------- | --------------------- | | **Abonnement** | Une adhésion qui se renouvelle chaque mois ou chaque année. | Un montant en devise. | | **Pack de crédits** | Une recharge ponctuelle de crédits du club, pour réserver. | Un montant en euros. | | **Option** | Un supplément ponctuel que le membre paie avec ses crédits. | Un nombre de crédits. | Les crédits sont la monnaie propre à votre club. Vous choisissez leur nom, comme Stars, et leur valeur sous **Crédits du club** dans [Paiements](/fr/guide/admins/payments). ## 2. Ajouter un plan [#2-ajouter-un-plan] Sélectionnez **Ajouter un plan**. 1. Saisissez un **Nom**. 2. Choisissez le **Type**. Les champs de prix de ce type apparaissent en dessous. 3. Si vous le souhaitez, saisissez une **Description**. Les membres voient le nom et la description. 4. Remplissez le prix : * **Abonnement** : le **Montant**, sa **Devise** et la **Périodicité de facturation**, **Mensuel** ou **Annuel**. * **Pack de crédits** : le **Montant (EUR)** et le nombre de **Crédits inclus**. * **Option** : le prix en crédits, par exemple **Prix en Stars ⭐**. 5. Sélectionnez **Créer un plan**. <figure> <img src="/images/guide/fr/admins/add-plan.png" alt="Fenêtre Ajouter un plan avec le Nom Family, le Type réglé sur Pack de crédits, la Description Shared top-up for families, 200 dans Montant (EUR) et 2600 dans Crédits inclus, et les boutons Annuler et Créer un plan." width="1280" height="800" /> <figcaption> La préparation d’un pack de crédits d’exemple, Family, avec 2 600 crédits pour 200 €. La commande à utiliser est entourée en orange. </figcaption> </figure> Le type, la devise et la périodicité de facturation ne sont plus modifiables une fois le plan créé. Pour les changer, créez un nouveau plan. ## 3. Modifier un plan [#3-modifier-un-plan] Sélectionnez le bouton crayon sur la ligne d’un plan, modifiez-le, puis sélectionnez **Enregistrer**. * Pour un **Pack de crédits** ou une **Option**, vous pouvez changer le nom, la description et le prix. * Pour un **Abonnement**, vous pouvez changer le nom et la description. La fenêtre vous le rappelle : **Pour changer le prix, scindez le plan.** ## 4. Changer le prix d’une adhésion [#4-changer-le-prix-dune-adhésion] Changer le prix d’une adhésion s’appelle scinder le plan. Sélectionnez **…** sur la ligne du plan, puis **Scinder**. Ce choix n’apparaît que pour un abonnement qui n’a jamais été scindé. * **Abonnés actuels** : les membres de ce plan y restent. Indiquez le **Prix** qu’ils paieront. Si vous le changez, ils sont prévenus aussitôt et paient le nouveau prix à partir de leur prochain paiement, ou dans un mois si ce paiement tombe avant. * **Nouveaux abonnés** : saisissez un **Nom du nouveau plan**, une **Description** et le **Prix**. Ce nouveau plan remplace l’ancien dans vos formulaires de [candidature](/fr/guide#application) et au paiement dès aujourd’hui. Sélectionnez **Scinder le plan** pour confirmer. <figure> <img src="/images/guide/fr/admins/split-plan.png" alt="Fenêtre Scinder pour le plan Adhésion 51 IÉNA (C) - Abonnement Cercle avec deux colonnes : Abonnés actuels, sans abonné actuel, qui garde le plan à 4500 euros, et Nouveaux abonnés avec un nouveau plan nommé Adhésion 51 IÉNA (C) - Abonnement Cercle 2027 au prix de 4800 euros, et un bouton Scinder le plan entouré en orange." width="1280" height="800" /> <figcaption> L’adhésion d’exemple Abonnement Cercle passe à 4 800 € pour les nouveaux membres, tandis que les membres actuels gardent 4 500 €. </figcaption> </figure> Après une scission, la ligne de l’ancien plan affiche **Réservé aux abonnés actuels. Remplacé par …** avec le nom du nouveau plan. > Pour augmenter le prix seulement pour les nouveaux abonnés, laissez le **Prix** sous **Abonnés > actuels** inchangé. ## 5. Supprimer un plan [#5-supprimer-un-plan] Sélectionnez **…** sur la ligne du plan, puis **Supprimer**, et confirmez avec **Supprimer le plan**. Les membres ne peuvent plus l’acheter. Les adhésions existantes conservent les informations du plan payé. ## Ce que voient les membres [#ce-que-voient-les-membres] Les membres choisissent une adhésion et achètent des packs de crédits et des options depuis **Mon compte**. Voir [Votre adhésion](/fr/guide/members/membership-and-credits) et [Vos crédits](/fr/guide/members/credits). Pour proposer une adhésion pendant que les personnes candidatent, ajoutez un bloc **Choix du plan** à vos [formulaires de candidature](/fr/guide/admins/onboarding-forms). # Administrer la plateforme Créez les espaces et clubs des nouveaux clients, choisissez leur propriétaire, désactivez des modules et suspendez un club. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/platform-administration Cette page s’adresse aux personnes qui gèrent Champagne, pas à l’équipe des clubs. Un administrateur de la plateforme crée un espace pour chaque client, y ajoute ses clubs, choisit son propriétaire et décide des modules utilisés par chaque club. Ce rôle ne donne accès ni aux membres, ni aux réservations, ni aux factures des clubs. ## 1. Ouvrir la section Plateforme [#1-ouvrir-la-section-plateforme] Dans le menu de gauche, sélectionnez **Plateforme**. Chaque carte est un espace : son propriétaire, puis ses clubs avec leur fuseau horaire et leur adresse hébergée. <figure> <img src="/images/guide/fr/admins/platform.png" alt="Section Plateforme montrant l’espace Champagne détenu par Demo Administrator, avec ses clubs Astel, Ukab Clubs et Vosges, chacun avec un bouton Modules et un interrupteur Actif." width="1280" height="800" /> <figcaption> L’espace de démonstration et ses trois clubs. </figcaption> </figure> ## 2. Créer l’espace d’un nouveau client [#2-créer-lespace-dun-nouveau-client] Sélectionnez **Nouvel espace**. 1. Saisissez le **Nom de l’espace**, en général la société du client. 2. Saisissez l’**E-mail du propriétaire**. 3. Décrivez le premier club : **Nom du club**, **Fuseau horaire** et, s’il reçoit des candidatures en ligne, son **Adresse hébergée**. 4. Sélectionnez **Créer l’espace**. <figure> <img src="/images/guide/fr/admins/platform-new-space.png" alt="Formulaire Nouvel espace rempli pour Riviera group, avec l’e-mail du propriétaire, le nom du club Riviera Nice, le fuseau Europe/Paris et l’adresse hébergée riviera-nice." width="1280" height="800" /> <figcaption> Un nouvel espace avec son premier club. </figcaption> </figure> Si l’e-mail a déjà un compte Champagne, cette personne devient propriétaire immédiatement. Sinon, elle reçoit une invitation par e-mail, valable 7 jours. Tant qu’elle ne l’a pas acceptée, la carte affiche l’invitation en attente et sa date d’expiration ; sélectionnez **Renvoyer l’invitation** pour en envoyer une nouvelle, ou **Annuler l’invitation** pour la retirer. Le propriétaire est administrateur de tous les clubs de l’espace et invite le reste de l’équipe depuis la section **Équipe**, comme décrit dans [Inviter votre équipe](/fr/guide/admins/invite-your-team). ## 3. Ajouter un club, modifier ou changer de propriétaire [#3-ajouter-un-club-modifier-ou-changer-de-propriétaire] * **Ajouter un club** ajoute un autre club à l’espace. Son propriétaire en devient administrateur. * **Renommer** sur un espace change son nom. **Modifier** sur la ligne d’un club change son nom, son fuseau horaire et son adresse hébergée. * Chaque espace a son propre nom, tout comme chaque club au sein d’un espace. Le nom d’un club commence toujours par une majuscule. * **Changer de propriétaire** confie l’espace à un autre e-mail. Le propriétaire actuel le reste jusqu’à ce que le nouveau accepte. **Retirer les accès de …** est coché par défaut : l’ancien propriétaire perd alors l’accès à tous les clubs de l’espace dès que le nouveau prend la main. Décochez-la pour le garder administrateur, par exemple un associé qui reste dans l’affaire. ## 4. Désactiver des modules pour un club [#4-désactiver-des-modules-pour-un-club] Sélectionnez **Modules** sur la ligne d’un club. Tous les modules sont actifs pour un nouveau club. Désactivez ceux que le client n’utilise pas, puis sélectionnez **Enregistrer**. <figure> <img src="/images/guide/fr/admins/platform-modules.png" alt="Modules de Ukab Clubs, avec un interrupteur pour Réservations, Disponibilités de l’équipe, Discussions clients, Entretien, Lettres d’information, Candidatures et Finances." width="1280" height="800" /> <figcaption> Les modules de Ukab Clubs. </figcaption> </figure> Un module désactivé disparaît du back-office de ce club pour tout le monde, administrateurs du club compris, et ses actions sont refusées. Le bouton indique ensuite le nombre de modules désactivés, par exemple **Modules (désactivés : 2)**. **Finances** est le seul module qui modifie aussi l’espace membre. Désactivez-le pour un club entièrement gratuit : * Les membres ne voient plus les crédits, les recharges, le choix de formule, le paiement ni les prix. Les étapes payantes de l’inscription sont ignorées, y compris dans les candidatures déjà en cours. * L’équipe ne voit plus les prix, les cautions, les produits payants ni les réglages de crédits, et ne peut plus ajouter d’étapes de formule à une inscription. * Champagne refuse tout nouveau paiement pour le club : paiement en ligne, recharges, dépense de crédits, cautions et remboursements. Les factures, soldes de crédits et adhésions payées existants sont conservés tels quels. Réactiver **Finances** les affiche à nouveau. ## 5. Suspendre un club [#5-suspendre-un-club] Désactivez l’interrupteur **Actif** d’un club, puis confirmez avec **Suspendre le club**. <figure> <img src="/images/guide/fr/admins/platform-suspend.png" alt="Confirmation demandant s’il faut suspendre Vosges, expliquant que son équipe et ses membres ne peuvent plus se connecter, avec un bouton Suspendre le club." width="1280" height="800" /> <figcaption> La suspension demande une confirmation. </figcaption> </figure> Un club suspendu garde toutes ses données, mais son équipe et ses membres ne peuvent plus s’y connecter et ses pages publiques ferment : site hébergé, réservation en ligne, candidatures et offres d’adhésion. Son équipe voit que le club est suspendu à la place du club, et les membres le voient sur la page de connexion. Réactivez l’interrupteur pour le réactiver : tout fonctionne de nouveau immédiatement. ## 6. Archiver un club ou un espace [#6-archiver-un-club-ou-un-espace] Quand un client part, ou n’a jamais fini sa mise en route, sélectionnez **Archiver** sur la ligne du club ou sur l’espace, puis confirmez. Un club archivé est fermé comme un club suspendu et quitte la liste ; archiver un espace archive tous ses clubs. Rien n’est supprimé. Activez **Afficher les archives** en haut de la page pour les retrouver, puis sélectionnez **Restaurer**. Restaurer un espace remet chaque club dans son état d’avant : un club actif rouvre, un club suspendu reste suspendu et un club archivé seul reste archivé. Un club d’un espace archivé ne revient qu’avec son espace. ## 7. Gérer les super admins [#7-gérer-les-super-admins] Ouvrez l’onglet **Super admins** en haut de la section Plateforme. Il liste toutes les personnes qui ont ce rôle. * Pour ajouter quelqu’un, saisissez son **E-mail** et sélectionnez **Ajouter le super admin**. Si la personne a déjà un compte Champagne, elle reçoit le rôle tout de suite. Sinon, saisissez aussi son **Nom** : elle reçoit un e-mail pour choisir son mot de passe, valable 7 jours. * Pour retirer le rôle, sélectionnez **Retirer le rôle** sur sa ligne et confirmez. Son compte et ses éventuels accès aux clubs restent. Vous ne pouvez pas vous retirer vous-même : il reste donc toujours au moins un super admin. Des outils comme Terraform créent et gèrent des clubs avec un jeton de plateforme plutôt qu’avec un compte. Ouvrez l’onglet **Jetons d’API**, sélectionnez **Créer un jeton**, saisissez son **Nom**, choisissez son **Accès** et, si vous le souhaitez, une **Date d’expiration (facultative)**, puis sélectionnez **Créer**. Copiez le jeton tout de suite : il n’est affiché qu’une seule fois. Un jeton de plateforme gère des clubs et ne lit aucune donnée de club. Pour couper l’accès d’un outil un moment, ouvrez le menu **⋯** sur la ligne de son jeton et sélectionnez **Bloquer** ; **Débloquer** lui rend l’accès. Pour le couper définitivement, sélectionnez **Supprimer** et confirmez. ## 8. Retrouver ce qui a changé [#8-retrouver-ce-qui-a-changé] L’onglet **Historique** liste toutes les modifications faites depuis la section Plateforme, de la plus récente à la plus ancienne : qui, quand, sur quel espace, club ou super admin, et les valeurs avant et après. L’équipe des clubs ne voit jamais ces entrées. # Configurer les catégories de réservation Créez ce que les membres peuvent réserver et décidez quand, pour combien de temps et combien de temps à l’avance ils peuvent le réserver. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/reservation-categories Tout ce que les membres peuvent réserver est organisé en [catégories de réservation](/fr/guide#reservation-category), comme un restaurant, des suites d’hôtel, des cours de tennis ou un cours de yoga. Chaque élément réservable d’une catégorie, comme une table, une suite ou un coach, est une ressource. Cette page montre comment créer une catégorie et définir ses règles de réservation. Pour donner un prix aux ressources et vendre des produits, voir [Ressources, prix et produits](/fr/guide/admins/resources-and-prices). Pour programmer des séances de groupe, voir [Séances, listes d’invités et périodes bloquées](/fr/guide/admins/sessions-and-blocked-periods). Vous travaillez depuis la section **Réservations**. Les boutons utilisés ici n’apparaissent que si votre [rôle](/fr/guide#role) vous permet de modifier les catégories. ## 1. Créer une catégorie [#1-créer-une-catégorie] Dans le menu de gauche, sélectionnez **Réservations**, puis le bouton **+** au bout de la barre des catégories. Quand votre club n’a encore aucune catégorie, la page affiche **Aucune catégorie de réservation pour le moment.** <figure> <img src="/images/guide/fr/admins/category-add.png" alt="Fenêtre d’ajout d’une catégorie de réservation à l’étape Configuration, avec Restaurant sélectionné sous Partir de, le nom La Brasserie, et les boutons Annuler et Suivant. Les étapes Ressources et Options sont listées à gauche." width="1280" height="800" /> <figcaption> Le début d’une catégorie de restaurant d’exemple, La Brasserie. La commande à utiliser est entourée en orange. </figcaption> </figure> 1. **Configuration** : choisissez de quoi **Partir de**. Chaque point de départ remplit des réglages adaptés que vous pourrez modifier ensuite. | Partir de | Ce qu’il prépare | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Personnalisé** | Rien : vous choisissez chaque réglage. | | **Restaurant** | Réservation selon le nombre de personnes, réservations de 2 heures commençant toutes les 30 minutes, réservables jusqu’à 60 jours à l’avance avec 60 minutes de délai, une liste d’attente, et des horaires d’ouverture pour le déjeuner et le dîner. | | **Cours collectif** | Réservation par séance, avec un billet à QR code. | | **Soin** | Réservations d’1 heure avec 2 heures de délai, des fiches client et des horaires d’ouverture de 09:00 à 19:00, du lundi au samedi. | | **Chambre ou suite** | Des listes de contrôle d’entretien pour chaque chambre. | Saisissez ensuite un **Nom** que les clients reconnaîtront, puis sélectionnez **Suivant**. 2. **Ressources** : saisissez le nom de chaque ressource, comme « Table 1 » ou « Suite jardin ». Sélectionnez **Ajouter une ressource** pour en ajouter une autre. Si vous laissez toutes les ressources vides, la catégorie reçoit une ressource à son propre nom. Sélectionnez **Suivant**. 3. **Options** : choisissez **Comment les membres réservent** et les outils facultatifs décrits dans les sections suivantes, puis sélectionnez **Créer la catégorie**. Utilisez **Retour** pour revenir à une étape précédente. Une fois la catégorie créée, sélectionnez-la dans la barre, puis sélectionnez **Paramètres de la catégorie** pour modifier quoi que ce soit. La fenêtre a cinq onglets : **Général**, **Ressources**, **Réservation**, **Heures** et **Sur place**. Sélectionnez **Enregistrer** pour appliquer vos modifications. ## 2. Choisir comment les membres réservent [#2-choisir-comment-les-membres-réservent] Dans l’onglet **Réservation**, **Comment les membres réservent** définit le déroulement de la réservation dans la catégorie : | Choix | Ce que font les membres | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **Une ressource et un horaire** | Ils choisissent une ressource, comme une suite, et les dates ou les horaires. | | **Un nombre de personnes et un horaire** | Ils indiquent le nombre de personnes et un horaire. La plus petite ressource libre qui convient leur est attribuée. | | **Une séance** | Ils rejoignent l’une des séances que vous programmez. Voir [Séances](/fr/guide/admins/sessions-and-blocked-periods). | | **Une demande à discuter** | Ils envoient une demande, puis échangent avec votre équipe avant qu’une date soit fixée, par exemple pour un cours particulier. | Deux interrupteurs complètent ce choix : * **Liste d’attente** : les membres peuvent attendre qu’une séance ou un horaire complet se libère. Disponible uniquement avec les séances ou la réservation selon le nombre de personnes. * **Non-membres** : les personnes sans adhésion peuvent réserver les ressources que vous leur ouvrez et payer par carte. Disponible uniquement quand les membres choisissent une ressource et un horaire. Voir [Prix](/fr/guide/admins/resources-and-prices#3-d%C3%A9finir-ses-prix). * **Réservable uniquement dans un parcours** : les membres ne voient plus la catégorie ; les candidats la réservent à l’étape de réservation d’un parcours, par exemple pour une visite découverte. L’équipe voit toujours ses réservations dans le planning. Incompatible avec **Non-membres**. Voir [Formulaires de candidature](/fr/guide/admins/onboarding-forms#4-choisir-les-%C3%A9tapes-apr%C3%A8s-acceptation). ## 3. Définir la durée et la période de réservation [#3-définir-la-durée-et-la-période-de-réservation] Le reste de l’onglet **Réservation** définit quand et pour combien de temps on peut réserver. <figure> <img src="/images/guide/fr/admins/category-booking.png" alt="Onglet Réservation des paramètres de Le Restaurant : Durée de réservation de 120 minutes, Horaires de début Toutes les 30 minutes, Réservable jusqu’à 60 jours à l’avance, Délai minimum de 60 minutes, et la section Catégories liées avec la case Cours collectif." width="1280" height="800" /> <figcaption> Les règles de réservation du restaurant d’exemple Le Restaurant. </figcaption> </figure> | Réglage | Ce qu’il fait | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Durée de réservation (minutes)** | La durée fixe d’une réservation. Les membres ne peuvent pas la changer. Laissez-la vide pour les laisser choisir la fin. Obligatoire pour la réservation selon le nombre de personnes. | | **Horaires de début** | Pour la réservation selon le nombre de personnes : à quelle fréquence une réservation peut commencer, de **Toutes les 5 minutes** à **Toutes les 60 minutes**. | | **Réservable jusqu’à (jours à l’avance)** | Combien de temps à l’avance les membres peuvent réserver. Laissez vide pour ne pas fixer de limite. | | **Délai minimum (minutes)** | Combien de temps avant le début une réservation doit être faite. Laissez vide pour ne pas fixer de limite. | | **Catégories liées** | D’autres catégories vers lesquelles une réservation peut être déplacée. L’équipe peut y déplacer des réservations, et elles sont proposées aux membres quand un horaire est complet. | ## 4. Définir les horaires d’ouverture [#4-définir-les-horaires-douverture] Dans l’onglet **Heures**, définissez les **Horaires d’ouverture** de chaque jour. Les membres et les non-membres ne peuvent réserver que pendant ces horaires. Votre équipe peut réserver à tout moment. <figure> <img src="/images/guide/fr/admins/category-hours.png" alt="Onglet Heures des paramètres de Le Restaurant : lundi marqué Fermé avec un bouton Ajouter des heures, mardi et mercredi ouverts de 12:00 à 14:30 et de 19:00 à 23:00, chaque plage avec un bouton corbeille, et le bouton Ajouter des heures du lundi entouré en orange." width="1280" height="800" /> <figcaption> Le restaurant d’exemple est fermé le lundi et ouvert pour le déjeuner et le dîner. </figcaption> </figure> * Sélectionnez **Ajouter des heures** sous un jour pour ajouter une heure d’ouverture et une heure de fermeture. Un jour peut avoir plusieurs plages, comme le déjeuner et le dîner. * Sélectionnez le bouton corbeille à côté d’une plage pour la retirer. Un jour sans horaires affiche **Fermé**. * Quand aucun jour n’a d’horaires, la catégorie affiche **Ouvert à tout moment.** ## 5. Activer les outils sur place [#5-activer-les-outils-sur-place] La plupart des outils se trouvent dans l’onglet **Sur place** des **Paramètres de la catégorie**. <figure> <img src="/images/guide/fr/admins/category-tickets.png" alt="Onglet Sur place des paramètres de la catégorie Événement : Billet avec QR code activé avec la description de scan à l’arrivée et entouré en orange, Entretien et Fiches client désactivés, et les boutons Supprimer la catégorie, Annuler et Enregistrer." width="1280" height="800" /> <figcaption> La catégorie d’exemple Événement donne un billet à QR code à chaque réservation. </figcaption> </figure> | Où | Interrupteur | Ce qu’il ajoute | | --------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Onglet **Ressources** | **Plan des espaces** | Un plan de l’espace sous le calendrier. Voir [Éditeur de plan des espaces](/fr/guide/admins/floor-plan-editor). | | Onglet **Sur place** | **Billet avec QR code** | Un billet que les membres montrent à l’arrivée. Voir [Enregistrer l’arrivée et le départ des clients](/fr/guide/staff/check-in-and-deposits). | | Onglet **Sur place** | **Entretien** | Une liste de contrôle et un état de nettoyage pour chaque ressource. Voir [Entretien](/fr/guide/staff/housekeeping). | | Onglet **Sur place** | **Fiches client** | Une fiche des préférences et des contre-indications de chaque client, par exemple pour les soins. | ## 6. Supprimer une catégorie [#6-supprimer-une-catégorie] Ouvrez les **Paramètres de la catégorie** et sélectionnez **Supprimer la catégorie**. La fenêtre vous demande de confirmer : la catégorie, ses séances et ses périodes bloquées sont supprimées, et c’est définitif. Sélectionnez de nouveau **Supprimer la catégorie** pour confirmer. # Ressources, prix et produits Listez ce qui peut être réservé dans une catégorie, définissez les règles et les prix de chaque ressource, et vendez des produits en complément. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/resources-and-prices Chaque élément réservable d’une [catégorie de réservation](/fr/guide#reservation-category), comme une table, une suite ou un coach, est une ressource. Cette page montre comment ajouter des ressources à une catégorie, définir leurs règles et leurs prix, et vendre des produits en complément, comme le minibar d’une suite. Pour créer la catégorie elle-même, voir [Configurer les catégories de réservation](/fr/guide/admins/reservation-categories). ## 1. Lister les ressources d’une catégorie [#1-lister-les-ressources-dune-catégorie] Dans le menu de gauche, sélectionnez **Réservations**, sélectionnez la catégorie dans la barre, puis **Paramètres de la catégorie**. Ouvrez l’onglet **Ressources**. * Renommez une ressource dans son champ. * Ajoutez-en une avec **Ajouter une ressource**. * Retirez-en une avec son bouton corbeille. Une ressource qui a déjà des réservations affiche un cadenas : vous pouvez la renommer, mais pas la supprimer. Dans une catégorie avec un plan des espaces, les ressources s’ajoutent et se retirent sur le plan : voir [Éditeur de plan des espaces](/fr/guide/admins/floor-plan-editor). Sélectionnez **Enregistrer** pour appliquer vos modifications. ## 2. Définir les règles de la ressource [#2-définir-les-règles-de-la-ressource] Sélectionnez le bouton de réglages à côté d’une ressource pour ouvrir ses propres réglages. L’onglet **Ressource** contient ses règles et ses prix. <figure> <img src="/images/guide/fr/admins/resource-settings.png" alt="Réglages de la Courtyard suite : Caution en Stars de 500, Nombre maximal d’occupants 4, champs d’arrivée et de départ vides, un Délai d’annulation de 2 jours avec son explication, l’interrupteur Réservable en externe activé, et une section Prix avec un prix par défaut de 250 Stars facturé Par jour, un prix Non-membres de 320 euros par jour et un bouton Ajouter un prix par plan entouré en orange." width="1280" height="800" /> <figcaption> La Courtyard suite d’exemple coûte 250 Stars par jour, ou 320 € pour les non-membres, avec une caution de 500 Stars et un délai d’annulation de 2 jours. La commande à utiliser est entourée en orange. </figcaption> </figure> | Réglage | Ce qu’il fait | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Équipe** | Les membres de l’équipe liés à cette ressource, comme le coach qui donne ces cours. | | **Caution en … (facultatif)** | Un montant en crédits retenu pour chaque réservation et remboursé ensuite. Voir [Paramètres du club](/fr/guide/admins/club-settings). | | **Nombre maximal d’occupants** | Combien de personnes la ressource accueille, de 1 à 51. | | **Arrivée par défaut (minutes avant)** | Les nouvelles réservations reçoivent une heure d’arrivée ce nombre de minutes avant leur début. | | **Départ par défaut (minutes après)** | Les nouvelles réservations reçoivent une heure de départ ce nombre de minutes après leur fin. | | **Délai d’annulation (jours avant)** | Les membres qui annulent plus tard que ce délai ne sont pas remboursés. | ## 3. Définir ses prix [#3-définir-ses-prix] 1. Saisissez le prix en [crédits](/fr/guide#credits) de votre club dans **Prix par défaut en …**, par exemple **Prix par défaut en Stars ⭐**. Il s’applique aux [plans](/fr/guide#plan) sans prix personnalisé. 2. Dans **Facturation**, choisissez **Par heure**, **Par jour**, **Par nuit** ou **Ponctuel**. 3. Pour donner à un plan d’adhésion son propre prix, sélectionnez **Ajouter un prix par plan**, choisissez le plan et saisissez son prix. Retirez-le avec son bouton corbeille. 4. Si la catégorie est ouverte aux non-membres, activez **Réservable en externe** et saisissez le prix en euros sur la ligne **Non-membres**. Les non-membres le paient par carte. 5. Sélectionnez **Enregistrer**. Pour créer vos plans et nommer vos crédits, voir [Plans et tarifs](/fr/guide/admins/plans-and-pricing) et [Paiements](/fr/guide/admins/payments). ## 4. Vendre des produits avec une ressource [#4-vendre-des-produits-avec-une-ressource] L’onglet **Produits** d’une ressource liste ce qui peut être ajouté à une réservation, comme une bouteille de champagne dans une suite. <figure> <img src="/images/guide/fr/admins/resource-products.png" alt="Onglet Produits du Penthouse défilé jusqu’à la Champagne bottle ouverte en modification : son image avec un bouton Supprimer l’image, le nom Champagne bottle, un prix de 65 Stars, une description, Consommation du minibar choisie comme facturation, un bouton Terminer la modification entouré en orange, et les boutons Ajouter un produit, Fermer et Enregistrer." width="1280" height="800" /> <figcaption> Le minibar d’exemple de la suite Penthouse. </figcaption> </figure> 1. Sélectionnez **Ajouter un produit**, ou **Modifier** sur un produit existant. 2. Sélectionnez **Importer une image** et choisissez une image PNG ou JPEG de 500 Ko au maximum. L’image est obligatoire. 3. Saisissez le **Nom du produit**, son **Prix du produit en …** crédits et, si vous le souhaitez, une **Description (facultative)**. 4. Sous **Comment ce produit est-il facturé ?**, choisissez : * **Supplément de réservation** : les clients ou l’équipe l’ajoutent à la réservation. * **Consommation du minibar** : l’équipe d’entretien déclare ce qui a été consommé. Ce produit n’est pas proposé au moment de réserver. 5. Sélectionnez **Terminer la modification**, puis **Enregistrer**. Pour retirer un produit de la liste, ouvrez-le et sélectionnez **Supprimer le produit**. Quand l’entretien est activé, la ressource a aussi les onglets **Liste de contrôle**, **État**, **Consommation** et **Historique**. Voir [Entretien](/fr/guide/staff/housekeeping). # Séances, listes d’invités et périodes bloquées Programmez les séances d’un cours collectif ou d’un événement, réservez-en certaines à une liste d’invités et fermez des périodes pour l’entretien. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/sessions-and-blocked-periods Dans une catégorie réservée par séance, comme un cours collectif ou un événement, les membres rejoignent les séances que vous programmez. Cette page montre comment programmer des séances, en réserver certaines à des membres choisis et fermer temporairement une catégorie ou une ressource. Pour créer la catégorie et choisir comment les membres réservent, voir Configurer les [catégories de réservation](/fr/guide/admins/reservation-categories). ## 1. Voir les séances d’une catégorie [#1-voir-les-séances-dune-catégorie] Dans le menu de gauche, sélectionnez **Réservations** et sélectionnez la catégorie dans la barre. Sélectionnez ensuite **Gérer les séances** en haut du calendrier. Une icône d’avertissement sur le bouton signale qu’aucune séance n’est encore programmée. <figure> <img src="/images/guide/fr/admins/manage-sessions.png" alt="Fenêtre Gérer les séances sur l’onglet Récurrents pour Cours collectif : sous Chaque jour, Open studio à 12:30 pendant 45 minutes dans le Main studio marqué Accès libre, puis des séances regroupées par jour, comme dimanche et lundi à 09:00, 15:00 et 18:00, de 60 minutes chacune dans le Main studio avec 8 places, chacune avec un bouton corbeille, et les boutons Fermer et Ajouter une séance, Ajouter une séance entouré en orange." width="1280" height="800" /> <figcaption> Les séances du cours d’exemple Cours collectif, avec l’Open studio quotidien auquel les membres viennent sans réserver. La commande à utiliser est entourée en orange. </figcaption> </figure> * **Récurrents** liste les séances qui se répètent chaque semaine ou chaque jour. Chaque ligne montre le lieu, puis le nombre de places, **Sans limite de participants** ou **Accès libre**, et **Liste d’invités** quand la séance est réservée à certaines personnes. * **Datés** liste les séances ponctuelles. Sélectionnez **Afficher les séances passées** pour voir les plus anciennes. Sélectionnez le bouton crayon sur une séance datée pour la modifier. * Sélectionnez le bouton corbeille sur une séance pour la supprimer, puis confirmez avec **Supprimer**. ## 2. Ajouter une séance [#2-ajouter-une-séance] Sélectionnez **Ajouter une séance**. 1. Sous **Quand**, choisissez **Ponctuel**, **Chaque semaine** ou **Chaque jour**, puis le **Jour** ou la **Date de début**, l’**Heure de début** et l’**Heure de fin**. Une séance peut finir après minuit : la fenêtre affiche alors **Se termine le lendemain.** 2. Sous **Détails**, saisissez un **Nom de la séance ou de l’événement (facultatif)** et choisissez le **Lieu**, la ressource où elle se déroule. 3. Sous **Accès**, choisissez la **Réservation** : * **Sur réservation** : les membres réservent une place. Indiquez une **Limite de membres (facultative)** pour plafonner le nombre de places. * **Accès libre** : les membres viennent sans réserver. La séance apparaît quand même dans leur planning. 4. Choisissez **Qui peut réserver** : **Tous les membres**, ou **Liste d’invités** pour réserver la séance à certaines personnes. Avec une liste d’invités, ajoutez des **Plans d’adhésion**, des **Personnes**, ou les deux. 5. Sélectionnez **Ajouter une séance**. <figure> <img src="/images/guide/fr/admins/session-form.png" alt="Fenêtre Ajouter une séance pour Cours collectif : chaque semaine le lundi de 12:30 à 13:30, le nom Founders' Pilates dans le Main studio, Sur réservation avec une limite de 10 membres, Qui peut réserver réglé sur Liste d’invités avec les champs Plans d’adhésion et Personnes vides, et le bouton Ajouter une séance entouré en orange." width="1280" height="800" /> <figcaption> Un cours hebdomadaire d’exemple réservé à une liste d’invités. Ajoutez les [plans](/fr/guide#plan) ou les personnes qui peuvent réserver avant d’enregistrer. </figcaption> </figure> ## 3. Réserver une séance datée à une liste d’invités [#3-réserver-une-séance-datée-à-une-liste-dinvités] Pour réserver une séance datée à certains membres, ouvrez-la avec son bouton crayon dans l’onglet **Datés**, choisissez **Liste d’invités** sous **Qui peut réserver** et ajoutez les plans ou les personnes. La fenêtre affiche **Cette modification s’applique uniquement à cette séance.** Sélectionnez **Enregistrer les modifications**. <figure> <img src="/images/guide/fr/admins/session-guest-list.png" alt="Fenêtre de modification de la séance Concert — soirée dans l’Event space, un samedi de 19:00 à 21:00 : Sur réservation avec une limite de 12 membres, Qui peut réserver réglé sur Liste d’invités, et Plans d’adhésion indiquant 2 sélectionnés, les plans Adhésion 51 IÉNA (C) - Abonnement Cercle et Adhésion 51 IÉNA (CCS) - Abonnement Cercle + Sport, chacun avec un bouton de retrait, et les boutons Annuler et Enregistrer les modifications, Enregistrer les modifications entouré en orange." width="1280" height="800" /> <figcaption> Le concert du soir de la catégorie d’exemple Événement est réservé à deux plans d’adhésion Cercle. </figcaption> </figure> Votre équipe suit les participants, la liste d’attente et la présence de chaque séance depuis l’**Agenda du club**. Voir [Utiliser l’agenda du club](/fr/guide/staff/planning). ## 4. Fermer des périodes et suivre les listes d’attente [#4-fermer-des-périodes-et-suivre-les-listes-dattente] * **Périodes bloquées**, en haut du calendrier dans les catégories sans séances, ferme temporairement toutes les ressources ou l’une d’elles, par exemple pour l’entretien. Personne ne peut réserver une période bloquée. * **Liste d’attente** apparaît dans les catégories réservées selon le nombre de personnes. Elle indique qui attend une place dans les 30 prochains jours. Les deux sont expliqués dans [Périodes bloquées, séances et listes d’attente](/fr/guide/staff/blocked-periods-and-waitlists). # Configurer votre club L’ordre à suivre pour configurer un nouveau club, de ses paramètres à son équipe. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/set-up-your-club Certains réglages dépendent d’autres : les [plans](/fr/guide#plan) demandent un compte Stripe, et un formulaire de [candidature](/fr/guide#application) propose les plans que vous avez créés. Suivez ces étapes dans cet ordre pour configurer un nouveau club. Chacune renvoie vers la page qui l’explique en détail. ### Remplir les paramètres du club [#remplir-les-paramètres-du-club]Indiquez l’adresse, le fuseau horaire, les champs des membres et les règles de réservation. Voir [Paramètres du club](/fr/guide/admins/club-settings). ### Connecter Stripe et nommer vos crédits [#connecter-stripe-et-nommer-vos-crédits]Les membres paient en ligne dès que votre club a un compte marchand Stripe. Voir [Paiements](/fr/guide/admins/payments). ### Créer vos plans [#créer-vos-plans]Ajoutez les adhésions, les packs de [crédits](/fr/guide#credits) et les options avec leurs prix. Voir [Plans et tarifs](/fr/guide/admins/plans-and-pricing). ### Préparer ce que les membres peuvent réserver [#préparer-ce-que-les-membres-peuvent-réserver]Créez les [catégories de réservation](/fr/guide#reservation-category), puis leurs ressources, leurs prix et leurs séances. Voir [Configurer les catégories de réservation](/fr/guide/admins/reservation-categories), [Ressources, prix et produits](/fr/guide/admins/resources-and-prices) et [Séances, listes d’invités et périodes bloquées](/fr/guide/admins/sessions-and-blocked-periods). ### Construire votre formulaire de candidature [#construire-votre-formulaire-de-candidature]Choisissez les questions auxquelles les candidats répondent et les étapes qu’ils terminent une fois acceptés. Voir [Formulaires de candidature](/fr/guide/admins/onboarding-forms). ### Publier le site de votre club [#publier-le-site-de-votre-club]Choisissez l’adresse web de votre club et publiez-y le formulaire de candidature. Voir [Le site de votre club](/fr/guide/admins/hosted-site). ### Inviter votre équipe [#inviter-votre-équipe]Donnez à chaque collègue l’accès au [back-office](/fr/guide#back-office) avec les bonnes autorisations. Voir [Inviter votre équipe](/fr/guide/admins/invite-your-team). # Profils et documents de votre équipe Tenez à jour le profil que voient les membres et les documents professionnels de chaque collègue. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/team-documents La fiche d’équipe de chaque collègue, dans **Équipe**, contient le profil que voient les membres et ses documents professionnels, comme la carte professionnelle ou les diplômes d’un coach. Sélectionnez un collègue dans **Équipe** pour ouvrir sa fiche : l’onglet **Profil** s’ouvre en premier. Pour trouver un collègue, voir [Gérer votre équipe](/fr/guide/admins/manage-your-team). ## 1. Modifier le profil que voient les membres [#1-modifier-le-profil-que-voient-les-membres] * **Biographie** montre le nom, la photo et le texte que voient les membres quand ils échangent avec ce collègue, par exemple un coach. Sélectionnez **Modifier** pour les changer, ou **Voir la fiche membre** pour voir le profil comme les membres le voient. * **Diplômes et documents** contient sa carte professionnelle, ses diplômes et ses autres justificatifs. ## 2. Ajouter un document [#2-ajouter-un-document] Pour ajouter un document : 1. Sélectionnez **Ajouter un document**. 2. Choisissez le **Type de document** : **Carte professionnelle**, **Diplôme** ou **Autre document**. 3. Saisissez un **Titre** et, si vous les avez, le **Numéro du document** et l’**Organisme émetteur**. 4. Saisissez la **Date de délivrance** et la **Date d’expiration**. La date d’expiration ne peut pas être antérieure à la date de délivrance. 5. Sélectionnez **Choisir un fichier** et choisissez le scan : un fichier PDF, JPEG ou PNG de 10 Mo au maximum. 6. Sélectionnez **Enregistrer le document**. <figure> <img src="/images/guide/fr/admins/team-add-document.png" alt="Fiche d’équipe de Sport coach sur l’onglet Profil avec le formulaire de document : Type de document Carte professionnelle, Titre Sports instructor card, Numéro du document CP-075-2024-1182, Organisme émetteur Préfecture de Paris, Date de délivrance 15/01/2024, Date d’expiration 15/01/2029, une zone Fichier avec un bouton Choisir un fichier, et les boutons Annuler et Enregistrer le document." width="1280" height="800" /> <figcaption> L’ajout d’une carte professionnelle d’exemple à la fiche de Sport coach. </figcaption> </figure> ## 3. Suivre les dates d’expiration [#3-suivre-les-dates-dexpiration] Chaque document indique combien de temps il reste valable : **Non expiré**, puis, à partir de 30 jours avant sa date d’expiration, un compte à rebours comme **Expire dans 20 jours**, **Expire demain** ou **Expire aujourd’hui**, et enfin **Expiré**. Les documents sont regroupés par type, avec le nombre de documents à côté de chaque type. <figure> <img src="/images/guide/fr/admins/team-documents.png" alt="Onglet Profil de Sport coach défilé jusqu’au groupe Diplôme avec 2 documents : Pool lifeguard certificate (BNSSA) du Ministry of Sports marqué Expiré, avec son numéro, sa date de délivrance, sa date d’expiration, sa date d’ajout et les boutons Aperçu, Télécharger, Modifier et Supprimer, puis First aid certificate (PSC1) de la French Red Cross marqué Expire dans 20 jours." width="1280" height="800" /> <figcaption> Les diplômes du coach d’exemple : un certificat expiré et un autre qui expire dans 20 jours. </figcaption> </figure> | Type de document | Qui le voit | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Carte professionnelle**, **Diplôme** | **Détails visibles des membres · Fichier privé** : les membres voient le type, le titre, l’organisme émetteur et les dates sous **Diplômes et carte professionnelle** sur le profil de ce collègue. Le fichier reste privé. | | **Autre document** | **Document interne** : seule votre équipe le voit. | Sur un document, sélectionnez **Aperçu** ou **Télécharger** pour ouvrir le fichier, **Modifier** pour changer ses informations ou **Remplacer le fichier**, et **Supprimer** pour le retirer. La suppression vous demande de confirmer avec **Supprimer le document**. > Utilisez régulièrement le filtre **Expire sous 30 jours** pour renouveler les cartes > professionnelles avant qu’elles n’expirent. # Qui voit quoi Ce que chaque autorisation affiche dans le back-office, pour choisir l’accès d’un collègue ou comprendre pourquoi une section manque. Source: https://docs.champagne.dev-ukab.fr/fr/guide/admins/who-sees-what Ce qu’un membre de l’équipe voit dans le [back-office](/fr/guide#back-office) dépend de son [rôle](/fr/guide#role) et, pour le rôle **Équipe**, des autorisations qu’un administrateur a cochées pour lui dans chaque club. Servez-vous de cette page pour choisir l’accès d’un collègue, ou pour trouver quelle autorisation fait réapparaître une section ou un bouton qui lui manque. ## Rôles [#rôles] * **Propriétaire** et **Administrateur** voient toutes les sections et tous les boutons, dans les clubs qui leur sont attribués. * **Équipe** ne voit que ce que ses autorisations permettent, dans les clubs qui lui sont attribués. Un membre de l’équipe sans aucune autorisation dans un club ne voit que son propre compte. * Seuls les propriétaires et les administrateurs voient la section **Équipe**. * Un module que la plateforme a [désactivé pour un club](/fr/guide/admins/platform-administration#4-d%C3%A9sactiver-des-modules-pour-un-club) est masqué pour tout le monde dans ce club, propriétaires et administrateurs compris, quelles que soient leurs autorisations. Un club suspendu disparaît du back-office de tous. Les administrateurs choisissent les autorisations quand ils [invitent un collègue](/fr/guide/admins/invite-your-team) et les modifient ensuite depuis [la liste de l’équipe](/fr/guide/admins/manage-your-team). Une modification s’applique dès qu’elle est enregistrée. ## Autorisations [#autorisations] Certaines autorisations en incluent une autre : cocher **Créer et modifier les fiches membres**, par exemple, donne aussi **Consulter les fiches membres**. La colonne « Donne aussi » les indique. ### Membres et candidatures [#membres-et-candidatures] | Autorisation | Ce qu’elle affiche | Donne aussi | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | **Consulter les fiches membres** | La section **Personnes** et les fiches des membres, en lecture seule. | | | **Créer et modifier les fiches membres** | **Ajouter une personne** et les invitations d’invités dans **Personnes**. Sur une fiche : **Modifier les informations**, la photo, les notifications, les prix des plans et les crédits, et **Déconnecter le membre**. | **Consulter les fiches membres** | | **Modifier les notes internes** | La rédaction de la note interne d’une fiche. | **Consulter les fiches membres** | | **Suspendre et bannir des membres** | La carte du statut sur une fiche : suspendre, bloquer les réservations, bannir, désactiver et rendre l’accès. | **Consulter les fiches membres** | | **Gérer les tags des membres** | L’ajout et le retrait de tags sur une fiche. | **Consulter les fiches membres** | | **Gérer les adhésions** | L’attribution et le retrait de plans sur une fiche. | **Consulter les fiches membres** | | **Traiter les candidatures** | La section **Candidatures**, et le lien d’une fiche vers la candidature de la personne. | | | **Rédiger et envoyer des lettres d’information** | La section **Lettres d’information**. | | ### Activités quotidiennes [#activités-quotidiennes] | Autorisation | Ce qu’elle affiche | Donne aussi | | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | **Consulter le planning attribué** | L’**Agenda du club**, pour les catégories de réservation attribuées à la personne. | | | **Créer et modifier les réservations** | La création de réservations depuis l’agenda, leur modification, leur déplacement et leur annulation, l’annulation de séances et le retrait de participants. | **Consulter le planning attribué** | | **Valider les présences et les billets** | Le contrôle des billets à l’entrée, l’enregistrement des départs et le pointage des présences. | **Consulter le planning attribué** | | **Gérer les messages de réservation** | Les messages des réservations qui demandent une conversation, dans **Conversations** et sur la réservation. | **Consulter le planning attribué** | | **Discuter avec les clients** | Les **Conversations** avec les membres et les clients, et **Nouvelle conversation** sur une fiche. | **Consulter les fiches membres** | | **Gérer le housekeeping et les consommations** | La section **Entretien**, pour les catégories qui l’utilisent. | **Consulter le planning attribué** | | **Gérer ses propres disponibilités** | **Mes disponibilités**, pour indiquer les horaires où la personne peut travailler. | | ### Finances [#finances] | Autorisation | Ce qu’elle affiche | Donne aussi | | ------------------------------------ | ----------------------------------------------------- | ----------- | | **Gérer les factures et les avoirs** | La section **Finances** : les factures et les avoirs. | | ### Configuration du club [#configuration-du-club] | Autorisation | Ce qu’elle affiche | Donne aussi | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | **Configurer les plans et les ressources** | Les sections **Plans** et **Réservations** : plans, catégories de réservation, ressources, plans des espaces et périodes bloquées. | **Consulter le planning attribué** | | **Gérer les paramètres et les intégrations** | La section **Paramètres**, les formulaires de candidature dans **Candidatures**, la connexion de la facturation dans **Finances** et la gestion des tags. | | ## Sans autorisation particulière [#sans-autorisation-particulière] * **Mes réservations** s’affiche pour les membres de l’équipe qu’on peut réserver eux-mêmes, comme les coachs, sans autorisation particulière. * Créer une réservation depuis l’agenda demande à la fois **Créer et modifier les réservations** et **Consulter les fiches membres**, pour choisir la personne concernée. * Un membre de l’équipe qui a **Consulter le planning attribué** sans aucune catégorie de réservation attribuée n’a pas d’**Agenda du club** : demandez à un administrateur de cocher ses catégories dans **Catégories du planning accessibles**. ## Restrictions [#restrictions] Deux cases dans l’onglet **Accès** d’un membre de l’équipe, sous **Restrictions**, limitent un compte quels que soient son rôle et ses autorisations. Elles s’appliquent à tout le compte, dans chaque club. * **Lecture seule** : le compte voit tout ce que son rôle et ses autorisations permettent et ne peut rien modifier, hormis son propre nom, son mot de passe et son profil. * **Masquer les données personnelles** : les personnes apparaissent sous un pseudonyme tel que Member 4F2A1C, identique sur tous les écrans. Coordonnées, photos, notes, documents et messages sont masqués, et la recherche de personnes par nom est indisponible. Cocher les deux pour un administrateur donne à une équipe technique un accès à votre club sans lui permettre de le modifier ni de savoir qui sont vos membres. Le propriétaire de l’espace ne peut pas être restreint. # Quand votre compte est limité Comprendre pourquoi vous ne pouvez pas réserver ou voir certaines pages, et ce que vous pouvez encore faire. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/account-status Votre club peut limiter votre compte, par exemple après plusieurs absences à vos réservations, et certaines pages demandent une adhésion valide. Cette page explique les avis que vous voyez alors dans votre [espace membre](/fr/guide#member-app). Pour le reste de l’espace membre, consultez [Votre espace membre](/fr/guide/members/your-account). ## 1. Lire le statut de votre compte [#1-lire-le-statut-de-votre-compte] Quand votre club limite votre compte, le statut à côté de votre nom change, et une carte **Statut du compte** en haut de chaque page l’explique. Quand le club a donné un motif, il apparaît sous **Motif**. <figure> <img src="/images/guide/fr/members/account-suspended.png" alt="Espace membre de Camille Roussel avec le statut Suspendu, une carte Statut du compte indiquant Votre accès est suspendu jusqu’au 31 décembre 2026 avec le motif Demo: temporary access suspension, et seulement les onglets Aperçu et Facturation." width="1280" height="800" /> <figcaption> La membre d’exemple Camille Roussel, suspendue jusqu’à la fin de l’année. </figcaption> </figure> | Statut | Ce qu’indique la carte | Ce que vous pouvez encore faire | | ------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Suspendu** | **Votre accès est suspendu jusqu’au** suivi d’une date | Voir votre aperçu, votre facturation et votre boîte de réception. Les réservations et les invitations sont masquées. | | **Réservations bloquées** | **Vous ne pouvez pas réserver jusqu’au** suivi d’une date | Tout sauf les nouvelles réservations. Vos réservations existantes restent accessibles. | | **Banni** | **Votre compte a été banni.** | Voir votre aperçu, votre facturation et votre boîte de réception. | | **Désactivé** | **Votre compte est actuellement désactivé.** | Voir votre aperçu, votre facturation et votre boîte de réception. | Contactez votre club si un statut vous semble erroné : seul le club peut le changer. ## 2. Quand vous ne pouvez pas réserver [#2-quand-vous-ne-pouvez-pas-réserver] Quand vos réservations sont bloquées, l’onglet **Réservations** affiche **Vos réservations sont actuellement bloquées** à la place du planning du club. Vos réservations à venir sont listées en dessous. <figure> <img src="/images/guide/fr/members/bookings-blocked.png" alt="Avertissement indiquant Vos réservations sont actuellement bloquées. Vos réservations existantes restent disponibles ci-dessous." width="1056" height="68" /> <figcaption> L’avis d’un membre dont les réservations sont bloquées après des absences répétées. </figcaption> </figure> Sans adhésion valide, l’onglet **Réservations** affiche **Une adhésion valide est nécessaire pour réserver** à la place du planning du club. Sélectionnez **Voir mon adhésion** pour choisir un [plan](/fr/guide#plan) : consultez [Votre adhésion](/fr/guide/members/membership-and-credits). <figure> <img src="/images/guide/fr/members/membership-required.png" alt="Avertissement indiquant Une adhésion valide est nécessaire pour réserver. Vérifiez votre adhésion avant de faire une nouvelle réservation. Vos réservations existantes restent disponibles ci-dessous, avec un bouton Voir mon adhésion entouré en orange." width="1056" height="100" /> <figcaption> L’onglet Réservations de Louis Boulanger, qui n’a pas encore d’adhésion. </figcaption> </figure> ## 3. Comptes invités et temporaires [#3-comptes-invités-et-temporaires] Les invités et les visiteurs voient une carte de plus : * **Accès invité** indique à une personne invitée par un membre jusqu’à quand elle peut profiter du club, par exemple **Vous êtes invité par** suivi du nom du membre et de la date de fin. Le compte temporaire se ferme ensuite automatiquement. Consultez [Accepter une invitation reçue par e-mail](/fr/guide/members/invitations-from-email). * **Compte temporaire** indique à un visiteur qui a réservé en ligne que le compte prend fin à la fin d’un jour donné. Consultez [Réserver en ligne en tant que visiteur](/fr/guide/members/book-online). <figure> <img src="/images/guide/fr/members/guest-account.png" alt="Espace membre de Clara Fontaine avec les mentions Actif et Compte invité, une carte Accès invité indiquant que Clara est invitée par Alex Morgan jusqu’à la date de fin de son accès et que son compte temporaire se ferme ensuite automatiquement, les onglets Aperçu et Réservations, la carte Je suis dans le bâtiment et une carte Prochaine réservation indiquant Aucune réservation." width="1280" height="800" /> <figcaption> Clara Fontaine, en visite chez Ukab Clubs comme invitée d’Alex Morgan. </figcaption> </figure> # Après la décision du club Lire la réponse du club, payer et signer ce qu’il reste après l’acceptation, et ouvrir votre espace membre. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/after-your-application Une fois votre [candidature](/fr/guide#application) envoyée, le club l’étudie et vous prévient par e-mail quand il a pris sa décision. Ouvrez votre candidature pour voir le résultat. Pour remplir et envoyer votre candidature, consultez [Remplir et signer votre candidature](/fr/guide/members/sign-your-application). ## 1. Si le club refuse votre candidature [#1-si-le-club-refuse-votre-candidature] Si le club refuse votre candidature, elle affiche **Candidature non acceptée**, avec le message du club sous **Motif indiqué par le club** quand il en a laissé un. Contactez le club si vous avez des questions. <figure> <img src="/images/guide/fr/members/application-rejected.png" alt="Candidature de Maya Demo avec le statut Refusée, le titre Candidature non acceptée et un motif indiqué par le club." width="1152" height="360" /> <figcaption> Une candidature d’exemple refusée, avec le motif du club. </figcaption> </figure> ## 2. Terminer les étapes après l’acceptation [#2-terminer-les-étapes-après-lacceptation] Si le club accepte votre candidature, la page affiche **Votre candidature est acceptée** et les étapes qui restent à faire après l’acceptation, chacune marquée **Terminé**, **À faire** ou **À venir**. La carte **Prochaine action · Étape …** à droite ouvre l’étape en cours : * Une étape de paiement affiche votre [plan](/fr/guide#plan) et son prix. Sélectionnez le bouton **Payer … avec Stripe**, qui indique le montant. Stripe, le service de paiement sécurisé, s’ouvre pour encaisser votre paiement, puis vous ramène à votre candidature. * Une étape de signature fonctionne comme décrit dans [Signer un document](/fr/guide/members/sign-your-application#2-signer-un-document). * Une étape d’adresse demande votre adresse postale, et une étape de téléphone votre numéro de mobile et le code reçu par SMS. Les deux sont enregistrés sur votre profil. * Une étape d’identité demande votre carte d’identité, votre passeport ou votre permis de conduire, et souvent un selfie. Sélectionnez **Vérifier mon identité** : Stripe, le service de vérification du club, s’ouvre pour les photographier, puis vous ramène. Pendant que Stripe les contrôle, l’étape affiche **Stripe vérifie votre document** ; la page se met à jour seule. * Une étape de visite vous permet de réserver une première visite au club. Choisissez une **Date**, sélectionnez un horaire, puis **Réserver cet horaire**. Les horaires complets sont barrés. Tant que vous n’avez pas sélectionné **Continuer**, vous pouvez **Changer l’horaire** ou **Annuler la réservation**. L’horaire réservé s’affiche aussi sous l’étape dans la liste, et vous recevez l’e-mail de confirmation et le rappel habituels. ### En cas de problème [#en-cas-de-problème] | Message | Que faire | | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | | **Échec de la signature**, avec **Yousign n’a pas pu terminer la signature. Veuillez réessayer.** | Sélectionnez **Continuer vers Yousign** pour signer à nouveau. | | **Vous avez refusé la signature. Vous pourrez recommencer lorsque vous serez prêt.** | Vous avez refusé de signer dans Yousign. Sélectionnez **Continuer vers Yousign** quand vous êtes prêt. | | **Votre signature n’est pas encore confirmée. Terminez la signature dans Yousign, puis vérifiez à nouveau.** | Terminez la signature dans l’onglet Yousign, puis sélectionnez **Vérifier la signature**. | | **Paiement annulé**, **Aucun paiement n’a été confirmé. Vous pouvez réessayer.** | Vous avez quitté la page Stripe sans payer. Sélectionnez à nouveau le bouton **Payer … avec Stripe**. | | **La session de paiement a expiré. Lancez un nouveau paiement.** | La page de paiement est restée ouverte trop longtemps. Sélectionnez **Lancer un nouveau paiement**. | | **Le paiement est actuellement indisponible. Contactez le club pour terminer cette étape.** | Le club ne peut pas encore encaisser le paiement en ligne. Contactez-le. | | **Nous n’avons pas pu identifier votre paiement. Contactez le club avant de réessayer.** | Ne payez pas deux fois : demandez au club si votre paiement est bien arrivé. | | **Votre identité n’a pas pu être vérifiée**, avec une raison donnée par Stripe | Suivez la raison, par exemple utilisez un document valide, puis sélectionnez **Réessayer**. | | **Vous n’avez pas l’âge minimum** | Votre document indique que vous n’avez pas l’âge minimum du club. Contactez le club s’il s’agit d’une erreur. | | **Confirmez votre e-mail pour ouvrir cette candidature.** | Le lien s’est ouvert dans un navigateur qui ne vous connaît pas. Sélectionnez **Retrouver ma candidature** pour recevoir un code. | En revenant de Stripe ou d’un lien reçu par e-mail, votre candidature peut s’ouvrir dans un onglet qui n’y a pas accès. Si elle indique **Votre paiement a bien été reçu. Retrouvez votre candidature avec votre e-mail pour continuer.**, sélectionnez **Retrouver ma candidature** et confirmez votre e-mail pour poursuivre. ## 3. Ouvrir votre espace membre [#3-ouvrir-votre-espace-membre] Quand toutes les étapes sont terminées et que vous n’avez pas encore de compte, choisissez votre mot de passe dans **Choisissez votre mot de passe** et sélectionnez **Enregistrer le mot de passe et ouvrir mon compte**, ou **Enregistrer le mot de passe et me connecter** sur le site de candidature du club. Consultez [Se connecter, activer votre compte et réinitialiser votre mot de passe](/fr/guide/members/passwords-and-activation). Votre [espace membre](/fr/guide#member-app) s’ouvre alors, ou sa page de connexion si vous avez candidaté sur le site du club. Quand vous rouvrez votre candidature plus tard, elle affiche **Bienvenue au club** : sélectionnez **Accéder à mon compte** pour ouvrir votre espace membre. <figure> <img src="/images/guide/fr/members/application-welcome.png" alt="Candidature de Sami Bernard avec le statut Acceptée, le titre Bienvenue au club, un bouton Accéder à mon compte entouré en orange, un lien Mot de passe oublié et une étape terminée." width="1152" height="460" /> <figcaption> La candidature terminée du membre d’exemple Sami Bernard. </figcaption> </figure> # Facturation Payer ce que vous devez et télécharger vos factures. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/billing Votre club vous envoie des factures pour votre adhésion et d’autres frais. Ouvrez **Mon compte**, sélectionnez l’onglet **Facturation**, puis **Factures** pour payer ce qui est dû et télécharger vos factures. ## 1. Payer une facture en attente [#1-payer-une-facture-en-attente] **Factures à régler** liste ce qu’il vous reste à payer, avec le montant restant et la date d’échéance. <figure> <img src="/images/guide/fr/members/billing.png" alt="Page Factures de Sam Rivera : une facture à régler EFAVXZ3T-0007 de 4 500,00 € dont l’échéance est passée, avec des boutons Ouvrir le PDF et Payer sur Stripe, Payer sur Stripe étant entouré en orange, et la même facture dans Mes factures marquée En retard." width="1280" height="800" /> <figcaption> Une facture d’adhésion d’exemple en retard pour Sam Rivera. La commande à utiliser est entourée en orange. </figcaption> </figure> 1. Sélectionnez **Payer sur Stripe**. Une page de paiement sécurisée s’ouvre dans un nouvel onglet. 2. Payez la facture par carte. 3. Revenez à votre compte. Un paiement peut mettre un moment à apparaître : la facture quitte ensuite **Factures à régler**. Si une facture affiche **Contactez votre club pour payer.**, elle ne peut pas être payée en ligne : demandez à votre club comment la régler. ## 2. Trouver et télécharger vos factures [#2-trouver-et-télécharger-vos-factures] **Mes factures** liste toutes vos factures, de la plus récente à la plus ancienne, avec leur numéro, leur **Total**, leur date **Émise** et leur statut. Sélectionnez **Ouvrir le PDF** pour ouvrir une facture dans un nouvel onglet, puis téléchargez-la ou imprimez-la depuis cet onglet. **PDF indisponible** signifie qu’aucun PDF ne peut être ouvert pour cette facture. Utilisez **Précédent** et **Suivant** quand vous avez beaucoup de factures. <figure> <img src="/images/guide/fr/members/billing-paid.png" alt="Page Factures sans facture à régler et avec une facture payée EFAVXZ3T-0003 de 1 800,00 €, sa date d’émission et un bouton Ouvrir le PDF entouré en orange." width="1056" height="348" /> <figcaption> La facture d’adhésion d’exemple payée de Jamie Chen. </figcaption> </figure> | Statut | Sens | | -------------------------- | ----------------------------------------------------------- | | **En attente de paiement** | La facture attend votre paiement. | | **En retard** | La date d’échéance est passée et la facture reste à payer. | | **Payée** | La facture est entièrement payée. | | **Brouillon** | Votre club prépare encore la facture. | | **Annulée** | Le club a annulé la facture : vous n’avez rien à payer. | | **Irrécouvrable** | Le club ne s’attend plus à ce que cette facture soit payée. | Si la page indique **Les factures sont actuellement indisponibles. Contactez votre club.**, la facturation en ligne du club n’est pas configurée : demandez vos factures au club. > Les réservations se paient depuis leur propre page avec **Payer la réservation** : consultez > [Faire une réservation](/fr/guide/members/book-a-reservation). Les achats de > [crédits](/fr/guide#credits) apparaissent dans votre historique de crédits : consultez [Vos > crédits](/fr/guide/members/credits). # Faire une réservation Choisir une activité dans le planning de votre club, choisir un horaire et confirmer votre réservation. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/book-a-reservation Les membres réservent des salles, des places et des séances depuis leur [espace membre](/fr/guide#member-app). Vous avez besoin d’une adhésion active dans votre club pour réserver. Sans compte, utilisez **Réserver en ligne** pour réserver en tant que visiteur : consultez [Réserver en ligne en tant que visiteur](/fr/guide/members/book-online). ## 1. Ouvrir vos réservations [#1-ouvrir-vos-réservations] Connectez-vous à l’espace membre, ouvrez **Mon compte** et sélectionnez l’onglet **Réservations**. Le haut de la page liste **Mes réservations à venir**. Sélectionnez une réservation pour voir ses détails, la payer ou écrire à votre club. <figure> <img src="/images/guide/fr/members/reservations.png" alt="Onglet Réservations de l’espace membre d’Alex Morgan listant les réservations à venir avec leur date, leur statut et leur prix : un cours Cours collectif avec la mention Liste d’attente · n° 3 et un bouton Quitter, une Suite d’hôtel marquée Réservée — paiement dû avec un bouton Payer la réservation entouré en orange, une réservation Place à la piscine elle aussi marquée Réservée — paiement dû avec son propre bouton Payer la réservation, et le début d’une réservation Cours particulier." width="1280" height="800" /> <figcaption> Réservations à venir du membre d’exemple Alex Morgan chez Ukab Clubs. La commande à utiliser est entourée en orange. </figcaption> </figure> Chaque réservation affiche un statut et son prix, avec la **Caution** quand le club en demande une. **Réservée — paiement dû** signifie que la place vous est gardée et reste à payer : sélectionnez **Payer la réservation** pour la régler. Un cours dont le club a proposé l’horaire affiche **Voir la proposition** à la place. Sans adhésion valide, la page affiche **Une adhésion valide est nécessaire pour réserver** à la place du planning du club. Consultez [Quand votre compte est limité](/fr/guide/members/account-status). ## 2. Trouver un horaire libre dans le planning du club [#2-trouver-un-horaire-libre-dans-le-planning-du-club] Faites défiler jusqu’à **Planning du club**. 1. Dans **Activité**, choisissez ce que vous voulez réserver, par exemple une salle de réunion. 2. Utilisez les flèches ou **Cette semaine** pour passer d’une semaine à l’autre. 3. Si l’activité compte plusieurs salles ou places, choisissez-en une dans **Ressource**. Chaque jour marque les horaires déjà pris **Indisponible**. Sélectionnez **Choisir un horaire** le jour voulu. <figure> <img src="/images/guide/fr/members/club-schedule.png" alt="Planning du club avec l’activité Salle de réunion et la ressource Conference room, montrant une semaine où chaque jour a des créneaux Indisponible et un bouton Choisir un horaire, l’un d’eux entouré en orange." width="1056" height="538" /> <figcaption> Une semaine de la salle de réunion d’exemple, Salle de réunion. </figcaption> </figure> Certaines activités fonctionnent autrement. Les cours collectifs listent directement leurs séances : vous vous inscrivez à une séance. Les places sur un plan des espaces, comme les transats de la piscine, affichent **Choisir une place** pour que vous choisissiez votre emplacement sur le plan. ## 3. Confirmer votre réservation [#3-confirmer-votre-réservation] Un formulaire s’ouvre avec l’activité et la ressource choisies. Indiquez le **Nombre de personnes**, la **Date de début** et l’**Heure de début**, puis la **Date de fin** et l’**Heure de fin**. Le **Total** affiche le prix avant votre confirmation. <figure> <img src="/images/guide/fr/members/booking-form.png" alt="Formulaire de réservation de la Conference room avec le nombre de personnes, les dates et heures de début et de fin, un total Gratuit et un bouton Réserver entouré en orange." width="448" height="493" /> <figcaption> Le formulaire de réservation de la Conference room d’exemple, gratuite pour les membres. </figcaption> </figure> Certaines activités proposent des extras, comme des boissons apportées à votre transat. Ils sont listés sous **Produits** avec leur prix : indiquez une **Quantité** pour chacun de ceux que vous voulez. Leur coût s’ajoute au total. <figure> <img src="/images/guide/fr/members/booking-products.png" alt="Section Produits d’un formulaire de réservation avec Sparkling water, served chilled, 50 cl, à 3,5 Stars, et une Quantité de 2." width="416" height="117" /> <figcaption> Deux eaux pétillantes ajoutées à une réservation d’exemple à la piscine. </figcaption> </figure> Sous les dates, un récapitulatif résume ce que vous allez payer : | Ligne | Ce que cela signifie | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ | | **Réservation** et **Caution remboursable** | Le prix et la caution, affichés séparément quand le club en demande une. | | **Total** ou **Total avec caution** | Le coût total de la réservation. | | **Solde disponible** | Vos crédits, quand la réservation se paie en crédits. | | **Paiement nécessaire pour confirmer.** | La réservation est gardée jusqu’à votre paiement. | | **Sous réserve de validation du club.** | Le club confirme la réservation avant qu’elle soit définitive. | | **Annulation gratuite jusqu’au …** | Annulez avant cette date pour récupérer le prix. | | **Le prix n’est pas remboursable : le délai d’annulation est dépassé.** | Annuler maintenant ne rembourse pas le prix. | | **Il manque … pour payer.** | Votre solde est trop bas pour payer la réservation tout de suite. | <figure> <img src="/images/guide/fr/members/booking-cost.png" alt="Récapitulatif de réservation avec un Total de 37 Stars, un Solde disponible de 540 Stars et la mention Paiement nécessaire pour confirmer." width="416" height="81" /> <figcaption> Le récapitulatif de la réservation d’exemple à la piscine : 30 Stars pour la place et 7 Stars d’eau. </figcaption> </figure> Quand votre solde est trop bas, le récapitulatif affiche **Il manque … pour payer.** avec le montant qui vous manque. Si la réservation n’a pas besoin de la validation du club, il ajoute **Vous pouvez réserver maintenant et payer une fois vos crédits rechargés.** : vous pouvez quand même sélectionner **Réserver** et payer plus tard. Sélectionnez **Recharger mes crédits** pour ouvrir votre page de [crédits](/fr/guide#credits) et acheter un pack. Consultez [Vos crédits](/fr/guide/members/credits#1-recharger-vos-cr%C3%A9dits). <figure> <img src="/images/guide/fr/members/booking-shortfall.png" alt="Récapitulatif de réservation d’une suite avec une Réservation de 250 Stars, une Caution remboursable de 500 Stars, un Total avec caution de 750 Stars et un Solde disponible de 540 Stars, puis l’avertissement Il manque 210 Stars pour payer, la mention Vous pouvez réserver maintenant et payer une fois vos crédits rechargés, et un bouton Recharger mes crédits entouré en orange." width="352" height="397" /> <figcaption> Une nuit en suite coûte à Alex Morgan 750 Stars avec la caution, soit 210 de plus que son solde. </figcaption> </figure> Sélectionnez **Réserver**. La réservation apparaît dans **Mes réservations à venir**. Si elle a un prix, payez-la depuis cette liste avec **Payer la réservation**. Consultez [Vos réservations](/fr/guide/members/reservations-and-coaching#3-payer-une-r%C3%A9servation). > Certains clubs fixent le prix des réservations en Stars, les crédits du club. Vous achetez des > Stars par packs depuis votre compte. Consultez [Vos crédits](/fr/guide/members/credits). # Réserver en ligne en tant que visiteur Réserver une chambre ou une place dans un club sans adhésion et payer en ligne. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/book-online Certains clubs permettent aux visiteurs de réserver en ligne, par exemple une suite d’hôtel pour quelques nuits. Vous n’avez pas besoin d’adhésion : vous choisissez ce que vous réservez, saisissez vos coordonnées et payez par carte. Champagne crée un compte temporaire pour votre séjour afin que vous puissiez suivre votre réservation. ## 1. Choisir un club [#1-choisir-un-club] Sélectionnez **Réserver en ligne** dans la navigation en haut de l’[espace membre](/fr/guide#member-app). La page liste les clubs ouverts à la réservation en ligne. Sélectionnez le club voulu. <figure> <img src="/images/guide/fr/members/book-online-clubs.png" alt="Page Réserver en ligne listant trois clubs ouverts à la réservation en ligne : Astel, Ukab Clubs et Vosges, avec Ukab Clubs entouré en orange." width="1280" height="800" /> <figcaption> Les clubs de démonstration Astel, Ukab Clubs et Vosges, tous ouverts à la réservation en ligne. La commande à utiliser est entourée en orange. </figcaption> </figure> Quand un seul club accepte les réservations en ligne, sa page s’ouvre directement. Si aucun club ne les accepte encore, la page indique **Aucun club ne propose encore la réservation en ligne.** ## 2. Choisir quoi réserver et quand [#2-choisir-quoi-réserver-et-quand] La page du club montre ce que vous pouvez réserver. Quand le club propose plusieurs activités, sélectionnez l’un des onglets en haut. 1. Dans **Ressource**, choisissez une chambre ou une place. Chaque ligne indique son prix, le nombre de personnes qu’elle accueille et, quand le club en demande une, sa caution. Une ressource déjà prise à vos dates affiche **Indisponible sur cette période**. 2. Indiquez le **Nombre de personnes** quand la ressource accueille plus d’une personne. <figure> <img src="/images/guide/fr/members/book-online-resource.png" alt="Page Réserver en ligne d’Ukab Clubs pour Suite d’hôtel listant quatre suites à 320,00 € par jour jusqu’à 4 personnes avec une caution de 50,00 €, Courtyard suite sélectionnée et entourée en orange, et le Nombre de personnes réglé sur 2." width="768" height="588" /> <figcaption> Choix de la suite d’exemple Courtyard suite pour deux personnes. La commande à utiliser est entourée en orange. </figcaption> </figure> 3. Indiquez la **Date de début** et l’**Heure de début**, puis la **Date de fin** et l’**Heure de fin**. Certaines activités ont une durée fixe : vous choisissez alors seulement le début. 4. Vérifiez le **Total** et sélectionnez **Continuer**. <figure> <img src="/images/guide/fr/members/book-online-selection.png" alt="Dates et heures de début et de fin de la réservation, de 10:00 le jour du début à 10:00 deux jours plus tard, avec un Total de 1 010,00 €, la ligne Dont une caution remboursable de 50,00 € et un bouton Continuer entouré en orange." width="768" height="364" /> <figcaption> La suite d’exemple Courtyard suite chez Ukab Clubs, réservée pour deux personnes sur trois jours : 960,00 € pour le séjour plus 50,00 € de caution. </figcaption> </figure> **Continuer** devient disponible quand la ressource est libre et que les dates sont valides. Sous les dates, le formulaire vous prévient quand la fin est avant le début, quand le début est passé ou quand le séjour dépasse 62 jours. Sans compte, un séjour dure au plus 31 jours : un séjour plus long est refusé à la dernière étape. Quand la ressource a une caution, le **Total** l’inclut et la ligne **Dont une caution remboursable de …** en dessous indique le montant remboursable. Pour changer votre choix après **Continuer**, sélectionnez **Modifier**. ## 3. Saisir vos coordonnées [#3-saisir-vos-coordonnées] Sous **Vos coordonnées**, deux onglets vous permettent de choisir comment continuer : * **Réserver sans adhésion** crée un compte temporaire pour cette réservation. * **Se connecter** est destiné aux membres de ce club. ### Réserver sans adhésion [#réserver-sans-adhésion] 1. Saisissez votre **Prénom**, votre **Nom**, votre **E-mail** et votre **Téléphone**. 2. Choisissez un **Mot de passe** : au moins 12 caractères, avec une minuscule, une majuscule, un chiffre et un symbole. Il vous sert à vous reconnecter pendant votre séjour. Sélectionnez l’icône en forme d’œil pour l’afficher ou le masquer. 3. Sélectionnez **Continuer vers le paiement**. <figure> <img src="/images/guide/fr/members/book-online-details.png" alt="Section Vos coordonnées avec l’onglet Réserver sans adhésion sélectionné : la mention Votre compte temporaire prend fin après votre séjour, le prénom Camille, le nom Martin, un e-mail et un téléphone d’exemple, un mot de passe masqué et un bouton Continuer vers le paiement entouré en orange." width="688" height="489" /> <figcaption> Le formulaire de compte temporaire rempli pour la visiteuse d’exemple Camille Martin. </figcaption> </figure> Champagne réserve la ressource, vous connecte avec votre compte temporaire et ouvre la page de paiement. Si un compte existe déjà avec cet e-mail dans ce club, un message vous demande de vous connecter : utilisez l’onglet **Se connecter**. ### Se connecter en tant que membre [#se-connecter-en-tant-que-membre] Sélectionnez **Se connecter**, saisissez votre **E-mail** et votre **Mot de passe**, puis sélectionnez **Se connecter**. La page affiche alors le formulaire de réservation des membres pour la même activité, avec votre choix conservé. Vous réservez comme dans [Faire une réservation](/fr/guide/members/book-a-reservation). Si votre adhésion ne vous permet pas de réserver, la page indique qu’une adhésion valide est nécessaire. ## 4. Payer et suivre votre réservation [#4-payer-et-suivre-votre-réservation] Après **Continuer vers le paiement**, vous payez par carte sur la page de paiement sécurisée. À votre retour, votre réservation s’ouvre dans votre compte temporaire : | Message | Ce que cela signifie | | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | **Paiement reçu — votre réservation est confirmée.** | Votre paiement est passé et la réservation est confirmée. | | **Confirmation de votre paiement…** | Le paiement est en cours de vérification. La page se met à jour toute seule. | | **Paiement annulé. Votre réservation reste en attente de paiement.** | Vous avez quitté la page de paiement. Payez depuis la page de la réservation quand vous êtes prêt. | Si la page de paiement ne peut pas s’ouvrir, un message indique que votre réservation est enregistrée : payez-la depuis votre compte. Quand la réservation est gratuite, elle s’ouvre directement, sans étape de paiement. Votre compte temporaire affiche un avis **Compte temporaire** avec sa date de fin : à la fin du dernier jour de votre séjour. Il est ensuite supprimé, et une réservation qui attend encore son paiement est annulée. D’ici là, connectez-vous avec votre e-mail et votre mot de passe depuis **Mon compte** pour voir votre réservation et la payer. Si vous rouvrez **Réserver en ligne** en étant connecté avec ce compte, la page indique **Vous êtes connecté avec un compte temporaire.** et propose **Mes réservations**. <figure> <img src="/images/guide/fr/members/book-online-account.png" alt="Espace membre de Marc Lefort avec les mentions Actif et Compte temporaire, une carte Compte temporaire donnant le dernier jour du compte temporaire, seulement les onglets Aperçu et Réservations, la carte Je suis dans le bâtiment et une carte Prochaine réservation pour un séjour Suite d’hôtel à venir marqué Réservée — paiement dû." width="1280" height="800" /> <figcaption> Le compte temporaire du visiteur Marc Lefort, qui a réservé le Penthouse pour deux nuits. </figcaption> </figure> Le compte temporaire a deux onglets : **Aperçu** et **Réservations**. Ouvrez votre réservation pour voir son prix, la caution et la date jusqu’à laquelle vous pouvez annuler gratuitement, puis sélectionnez **Payer … par carte**. Consultez [Vos réservations](/fr/guide/members/reservations-and-coaching#2-lire-les-montants). > Si le club n’accepte pas les réservations en ligne, sa page indique **La réservation en ligne > n’est pas proposée par ce club.** Sélectionnez **Voir les clubs disponibles** pour revenir à la > liste. Si la page indique **La réservation en ligne est indisponible. Veuillez réessayer.**, > rechargez-la un peu plus tard. <figure> <img src="/images/guide/fr/members/book-online-closed.png" alt="Carte Réserver en ligne avec le message La réservation en ligne n’est pas proposée par ce club et un bouton Voir les clubs disponibles entouré en orange." width="688" height="164" /> <figcaption> La page d’un club qui n’accepte pas les réservations en ligne. </figcaption> </figure> # Vos crédits Recharger vos crédits du club, suivre votre historique de crédits et acheter des options. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/credits Les [crédits](/fr/guide#credits) sont la monnaie propre au club pour les réservations : chaque club leur donne son propre nom, par exemple Stars ⭐ chez Ukab Clubs. Ils se trouvent dans l’onglet **Facturation** de **Mon compte**. Pour votre adhésion, consultez [Votre adhésion](/fr/guide/members/membership-and-credits). ## 1. Recharger vos crédits [#1-recharger-vos-crédits] Sélectionnez **Crédits**. **Mes crédits** affiche votre solde. Sous **Recharger**, chaque pack indique le nombre de crédits obtenus et son prix. **Économisez** indique la remise par rapport à l’achat de crédits à l’unité. <figure> <img src="/images/guide/fr/members/credits.png" alt="Carte Mes crédits d’Alex Morgan avec un solde de 540 Stars, trois packs avec leurs boutons Recharger pour, le premier entouré en orange : Discovery 250 Stars pour 25,00 €, Weekend 600 Stars pour 50,00 € avec 16 % d’économie et Regular 1 250 Stars pour 100,00 € avec 20 % d’économie, et un Historique des crédits avec une recharge Weekend, trois réservations et un remboursement." width="1056" height="804" /> <figcaption> Le solde de 540 Stars et l’historique des crédits d’Alex Morgan chez Ukab Clubs. </figcaption> </figure> Sélectionnez **Recharger pour …** sur un pack et payez sur la page sécurisée qui s’ouvre. De retour sur la page, **Crédits ajoutés.** confirme la recharge. **Recharge envoyée · confirmation du paiement en attente.** signifie que le paiement est encore en cours de vérification. **Recharge annulée.** signifie que vous avez quitté la page de paiement sans payer : aucun crédit n’a été ajouté. Quand une réservation coûte plus de crédits que vous n’en avez, le formulaire de réservation et la page de la réservation proposent **Recharger mes crédits**. Si vous venez de la page de la réservation, sélectionnez **Revenir à ma réservation** après la recharge pour terminer son paiement. ## 2. Suivre votre historique de crédits [#2-suivre-votre-historique-de-crédits] **Historique des crédits** liste chaque mouvement, du plus récent au plus ancien, avec son objet, sa date, son montant et, pour un achat, le prix payé. Dans l’exemple ci-dessus, Alex a acheté le pack Weekend pour 50,00 €, dépensé 30 Stars pour Transat #2, récupéré les 80 Stars d’un cours de Pilates annulé, puis dépensé 30 Stars pour Transat #4. Utilisez **Précédent** et **Suivant** pour parcourir les mouvements plus anciens. | Ligne | Ce qui s’est passé | | --------------------- | ----------------------------------------------------------------- | | **Recharge** | Vous avez acheté un pack de crédits. | | **Réservation** | Des crédits ont été dépensés pour une réservation. | | **Remboursement** | Des crédits vous ont été rendus après une réservation annulée. | | **Caution** | Des crédits ont été mis de côté en garantie pour une réservation. | | **Caution restituée** | La caution est revenue sur votre solde. | | **Option** | Des crédits ont été dépensés pour une option. | > Certains clubs vendent des extras, appelés options. Elles apparaissent sous **Options** sur la > page **Adhésion** avec leur prix en crédits. Sélectionnez **Acheter pour …** : les crédits sont > aussitôt retirés de votre solde et le bouton affiche **Acheté**. Les options que vous possédez > sont listées sous **Mes options**. # Trouver un club et candidater Choisir un club, lire ses offres et commencer une candidature. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/find-a-club-and-apply Vous rejoignez un club en lui envoyant une [candidature](/fr/guide#application) depuis l’[espace membre](/fr/guide#member-app). Vous n’avez pas besoin de compte pour candidater : le club le crée pour vous dès qu’il accepte votre candidature. ## 1. Choisir un club [#1-choisir-un-club] Ouvrez l’espace membre sur l’onglet **Clubs**. Dans **Club**, saisissez le nom du club ou ouvrez la liste et sélectionnez-le. Le profil du club s’affiche avec son adresse et sa description. <figure> <img src="/images/guide/fr/members/clubs.png" alt="Onglet Clubs avec Ukab Clubs sélectionné dans le champ Club, entouré en orange, sa fiche de profil avec l’adresse, la description et un bouton Réserver en ligne, et le début de la section Parcours d’inscription." width="1280" height="800" /> <figcaption> Le profil du club d’exemple Ukab Clubs. La commande à utiliser est entourée en orange. </figcaption> </figure> Si le club accepte les réservations de visiteurs, son profil affiche **Réserver en ligne**. Vous pouvez l’utiliser pour réserver sans être membre. ## 2. Comparer les parcours d’inscription [#2-comparer-les-parcours-dinscription] Un parcours d’inscription est une façon de rejoindre le club, avec son propre formulaire et ses propres [plans](/fr/guide#plan). Un club peut en proposer plusieurs, par exemple un pour les membres habituels et un pour les membres fondateurs. Sous **Parcours d’inscription**, chaque carte indique le nombre d’étapes et les liste dans l’ordre, avec le type de réponse attendu à chaque étape et la mention **Facultatif** pour celles que vous pouvez passer. <figure> <img src="/images/guide/fr/members/club-onboardings.png" alt="Section Parcours d’inscription avec deux cartes, Common members en 7 étapes et Founding members en 4 étapes, chacune listant ses étapes avec un bouton Postuler en tant que et un bouton Ouvrir la page hébergée, Postuler en tant que Common members entouré en orange." width="1072" height="520" /> <figcaption> Les deux parcours d’inscription d’exemple d’Ukab Clubs. </figcaption> </figure> | Type d’étape | Ce que vous faites | | ------------------------------------------------- | -------------------------------------------------------------------------- | | **Nom** | Saisissez votre prénom et votre nom. | | **Vérification de l’e-mail** | Saisissez votre adresse e-mail et le code que le club vous envoie. | | **Réponse courte**, **Réponse longue**, **Choix** | Répondez à la question du club. | | **Choix de l’offre** | Choisissez le plan d’adhésion qui vous intéresse. | | **Parrains** | Saisissez les codes de parrainage que des membres du club vous ont donnés. | | **Adresse** | Saisissez votre adresse postale. | | **Vérification du téléphone** | Saisissez votre numéro de mobile et le code reçu par SMS. | | **Vérification d’identité** | Faites vérifier votre pièce d’identité par Stripe. | | **Réservation de visite** | Réservez une première visite au club. | | **Signature** | Signez un document en ligne, comme le règlement du club. | | **Paiement** | Payez votre plan en ligne. | Sous les parcours d’inscription, **Offres d’adhésion** liste les plans et les options du club avec leur prix. Vous choisissez votre plan pendant la candidature. <figure> <img src="/images/guide/fr/members/club-offers.png" alt="Section Offres d’adhésion avec trois abonnements annuels d’Ukab Clubs à 1 800, 4 500 et 6 300 euros par an." width="1072" height="252" /> <figcaption> Les abonnements annuels d’exemple d’Ukab Clubs. </figcaption> </figure> ## 3. Commencer votre candidature [#3-commencer-votre-candidature] Sélectionnez **Postuler en tant que** suivi du nom du parcours, par exemple le bouton **Postuler en tant que** de la carte Common members. Certains parcours affichent d’abord une courte page de présentation : sélectionnez **Commencer la candidature** pour continuer. Votre candidature s’ouvre sur sa première étape. Pour la remplir, consultez [Remplir et signer votre candidature](/fr/guide/members/sign-your-application). > Une carte marquée **Hébergé** peut aussi être remplie sur le site de candidature aux couleurs du > club. Sélectionnez **Ouvrir la page hébergée** pour y aller. Les clubs configurent ce site dans > [Le site de votre club](/fr/guide/admins/hosted-site). ## 4. Retrouver votre candidature [#4-retrouver-votre-candidature] Vos réponses sont enregistrées à chaque étape : vous pouvez donc vous arrêter et revenir plus tard. Pour rouvrir une candidature, ou suivre une candidature déjà envoyée : 1. Sélectionnez l’onglet **Candidatures**, ou **Retrouver ma candidature** en bas de l’onglet **Clubs**. 2. Vérifiez le **Club** et saisissez l’**Adresse e-mail** utilisée dans votre candidature. 3. Sélectionnez **Envoyer le code**. Le club vous envoie par e-mail un code à six chiffres, valable 10 minutes. 4. Saisissez le code dans **Code de vérification à six chiffres** et sélectionnez **Rechercher des candidatures**. 5. Sélectionnez **Ouvrir la candidature** à côté de la candidature voulue. <figure> <img src="/images/guide/fr/members/find-my-application.png" alt="Carte Retrouver ma candidature au milieu de la page avec Ukab Clubs dans le champ Club, une adresse e-mail d’exemple et un bouton Envoyer le code entouré en orange." width="1280" height="800" /> <figcaption> Recherche des candidatures d’une adresse e-mail d’exemple chez Ukab Clubs. </figcaption> </figure> Si l’e-mail n’arrive pas, attendez une minute et sélectionnez **Renvoyer le code**, ou sélectionnez **Modifier l’e-mail** pour corriger l’adresse. # Premiers pas De votre candidature à votre première réservation, les étapes d’un nouveau membre dans l’ordre. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/get-started Rejoindre un club avec Champagne se fait en quelques étapes. Suivez-les dans cet ordre : chacune renvoie vers la page qui l’explique en détail. ### Candidater auprès d’un club [#candidater-auprès-dun-club]Choisissez votre club, comparez ses offres et commencez votre [candidature](/fr/guide#application). Consultez [Trouver un club et candidater](/fr/guide/members/find-a-club-and-apply). ### Remplir et signer votre candidature [#remplir-et-signer-votre-candidature]Répondez à chaque étape, signez les documents du club et envoyez votre candidature. Consultez [Remplir et signer votre candidature](/fr/guide/members/sign-your-application). ### Terminer après la décision du club [#terminer-après-la-décision-du-club]Une fois accepté, payez et signez ce qu’il reste, puis choisissez votre mot de passe. Consultez [Après la décision du club](/fr/guide/members/after-your-application). ### Ouvrir votre espace membre [#ouvrir-votre-espace-membre]Repérez-vous dans **Mon compte** et mettez à jour votre profil. Consultez [Votre espace membre](/fr/guide/members/your-account). ### Faire votre première réservation [#faire-votre-première-réservation]Rechargez vos [crédits](/fr/guide#credits) si le club facture les réservations en crédits, puis réservez. Consultez [Vos crédits](/fr/guide/members/credits) et [Faire une réservation](/fr/guide/members/book-a-reservation). Vous n’êtes pas membre et voulez seulement réserver une fois ? Consultez [Réserver en ligne en tant que visiteur](/fr/guide/members/book-online). # Invités et parrainage Inviter une personne au club et parrainer quelqu’un qui candidate. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/guests-and-sponsorship En tant que membre, vous pouvez ouvrir le club aux personnes que vous connaissez. Invitez une personne pour quelques heures, ou donnez un code de parrainage à quelqu’un qui candidate pour devenir membre. Les deux se trouvent dans l’onglet **Invitations** de **Mon compte**. ## 1. Inviter une personne [#1-inviter-une-personne] Une personne invitée reçoit un compte temporaire dans votre club, par exemple pour vous rejoindre le temps d’un après-midi. Sélectionnez l’onglet **Invitations**, puis **Invités du club**. 1. Saisissez l’**E-mail de l’invité**. 2. Choisissez la **Durée d’accès** : **12 heures**, **24 heures** ou **48 heures**. Le compte temporaire démarre quand la personne accepte. 3. Sélectionnez **Envoyer l’invitation**. La page confirme **Invitation envoyée à …** et la personne reçoit un e-mail pour l’accepter. <figure> <img src="/images/guide/fr/members/guest-invitation.png" alt="Page Invités du club d’Alex Morgan avec le formulaire Inviter au club rempli avec sophie@example.com et 48 heures sélectionné, un bouton Envoyer l’invitation entouré en orange, et Mes invitations listant guest@example.com en Invitation en attente, avec Clara Fontaine en dessous." width="1280" height="800" /> <figcaption> Une nouvelle invitation de 48 heures pour [sophie@example.com](mailto:sophie@example.com), sous l’invitation en attente d’Alex Morgan. La commande à utiliser est entourée en orange. </figcaption> </figure> Si la personne a déjà une invitation en attente ou un accès actif, le formulaire indique **Une invitation est déjà en attente pour cet e-mail.** ou **Cet invité dispose déjà d’un accès actif.**, et **Envoyer l’invitation** reste indisponible. <figure> <img src="/images/guide/fr/members/guest-already-invited.png" alt="Formulaire Inviter au club avec guest@example.com, la note Une invitation est déjà en attente pour cet e-mail, et un bouton Envoyer l’invitation grisé. Mes invitations liste guest@example.com en Invitation en attente, avec 24 heures et la date d’expiration du lien, et Clara Fontaine en Accès invité actif, avec la fin de son accès." width="1056" height="513" /> <figcaption> [guest@example.com](mailto:guest@example.com) a déjà une invitation en attente d’Alex Morgan, et Clara Fontaine est au club comme son invitée. </figcaption> </figure> ## 2. Suivre vos invitations [#2-suivre-vos-invitations] **Mes invitations** liste les personnes que vous avez invitées. Sous chacune, une ligne donne la durée d’accès et la date à laquelle le lien cesse de fonctionner, par exemple **24 heures · lien valable jusqu’au** suivi d’une date. Une fois que la personne a accepté, la ligne affiche **Accès jusqu’au** et la fin de son accès. | Statut | Sens | | ------------------------- | ------------------------------------------ | | **Invitation en attente** | La personne n’a pas encore accepté. | | **Invitation expirée** | La personne n’a pas accepté à temps. | | **Accès invité actif** | La personne utilise son compte temporaire. | | **Accès invité terminé** | La durée d’accès est écoulée. | Pour une invitation expirée ou un accès terminé, sélectionnez **Réinviter**. Le formulaire se remplit avec le même e-mail et la même durée : vérifiez-les et sélectionnez **Envoyer l’invitation**. ## 3. Parrainer un nouveau membre [#3-parrainer-un-nouveau-membre] Certains clubs demandent aux candidats des codes fournis par des membres actuels. Sélectionnez **Parrainage**. <figure> <img src="/images/guide/fr/members/sponsor-codes.png" alt="Carte Codes de parrainage avec un bouton Générer un code entouré en orange, le code K7MZ-4QXP créé il y a quelques jours et marqué Disponible avec des boutons Copier et Révoquer, Copier étant aussi entouré en orange, et le code H3RT-W9AB marqué Utilisé par Lucas Demo le 2 septembre 2026." width="1056" height="277" /> <figcaption> Les codes de parrainage d’Alex Morgan : l’un encore disponible, l’autre utilisé par le candidat Lucas Demo. </figcaption> </figure> 1. Sélectionnez **Générer un code**. Un nouveau code apparaît dans la liste avec la mention **Disponible**. 2. Sélectionnez **Copier** et envoyez le code à la personne que vous parrainez. 3. Elle le saisit dans sa [candidature](/fr/guide#application), dans un champ comme **Code de parrainage 1**. Consultez [Remplir et signer votre candidature](/fr/guide/members/sign-your-application). Chaque code ne sert qu’une fois. Une fois utilisé, il affiche **Utilisé** avec le nom du candidat et la date, comme H3RT-W9AB ci-dessus. Pour annuler un code que vous ne voulez plus donner, sélectionnez **Révoquer**, puis de nouveau **Révoquer** dans **Révoquer ce code ?** : le code ne fonctionne plus, même pour une personne à qui vous l’avez déjà donné. Seuls les codes disponibles peuvent être copiés ou révoqués. Vous pouvez garder jusqu’à 5 codes que personne n’a encore utilisés. Au-delà, la page indique **Vous pouvez conserver au plus 5 codes de parrainage inutilisés. Révoquez-en un d’abord.** > Il vous faut une adhésion valide pour parrainer. Sans adhésion, la page affiche **Une adhésion > valide est nécessaire pour parrainer** et un lien **Voir mon adhésion**. # Accepter une invitation reçue par e-mail Ouvrir une invitation d’invité ou de réservation depuis son e-mail, et vous désabonner d’une lettre d’information. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/invitations-from-email Les clubs et leurs membres vous envoient des e-mails avec un lien : une invitation à venir au club en tant qu’invité, une invitation à rejoindre la réservation de quelqu’un, ou une lettre d’information. Chaque lien ouvre une page de l’[espace membre](/fr/guide#member-app) où vous répondez en quelques clics. ## 1. Accepter une invitation d’invité [#1-accepter-une-invitation-dinvité] Un membre ou le club peut vous inviter au club pour quelques heures. Vous recevez un e-mail, rédigé en anglais, intitulé « You are invited to … as a guest ». Ouvrez son lien **Accept the invitation** avant qu’il n’expire : l’e-mail indique le nombre de jours. La page **Vous êtes invité à …** indique qui vous a invité et pour combien d’heures. Votre adresse e-mail est déjà renseignée. 1. Saisissez votre **Prénom** et votre **Nom**. 2. Choisissez un **Mot de passe** et saisissez-le de nouveau dans **Confirmer le mot de passe**. Utilisez au moins 12 caractères, dont une minuscule, une majuscule, un chiffre et un symbole. Il vous sert à vous reconnecter tant que dure votre accès invité. 3. Sélectionnez **Accepter l’invitation**. <figure> <img src="/images/guide/fr/members/guest-invitation-accept.png" alt="Page Vous êtes invité à Ukab Clubs : Alex Morgan vous a invité pour 24 heures, l’e-mail guest@example.com, le prénom Robin, le nom Guest, un mot de passe et sa confirmation, et un bouton Accepter l’invitation entouré en orange." width="400" height="584" /> <figcaption> Une invitation du membre d’exemple Alex Morgan à [guest@example.com](mailto:guest@example.com), pour 24 heures. La commande à utiliser est entourée en orange. </figcaption> </figure> Votre compte temporaire démarre quand vous acceptez, et votre espace membre s’ouvre. Il affiche un avis **Accès invité** avec l’heure de fin de votre accès. Le compte se ferme ensuite automatiquement. Si la page indique **Invitation indisponible**, le lien est incomplet, déjà utilisé ou expiré. Demandez à la personne qui vous a invité de vous envoyer une nouvelle invitation, ou sélectionnez **Retour à l’accueil**. <figure> <img src="/images/guide/fr/members/guest-invitation-unavailable.png" alt="Carte Invitation indisponible indiquant que l’invitation d’invité n’est pas valide, a déjà été acceptée ou a expiré, avec un lien Retour à l’accueil entouré en orange." width="400" height="221" /> <figcaption> Un lien d’invitation d’invité qui ne fonctionne plus. </figcaption> </figure> ## 2. Répondre à une invitation à une réservation [#2-répondre-à-une-invitation-à-une-réservation] Quand un membre vous ajoute à sa réservation, vous recevez un e-mail intitulé « … · Reservation invitation ». Son lien ouvre la page **Invitation à une réservation** avec la personne qui vous a invité, le club, le lieu et l’horaire de la réservation. La suite dépend du club : | Ce que la page affiche | Que faire | | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | **Participation en tant que …** avec votre nom | Sélectionnez **Accepter l’invitation** pour participer, ou **Refuser**. | | Un champ **Votre nom** | Saisissez votre nom, puis sélectionnez **Accepter l’invitation**. Sélectionnez **Refuser** pour décliner. | | **Créer un compte invité** et **Refuser** | Le club demande aux invités d’avoir un compte. Sélectionnez **Créer un compte invité** et suivez les étapes d’invitation de la section 1. | <figure> <img src="/images/guide/fr/members/reservation-invitation.png" alt="Page Invitation à une réservation : Alex Morgan vous a invité à rejoindre sa réservation chez Ukab Clubs, Transat #4, avec la date et l’heure, Participation en tant que Jamie Chen, et des boutons Accepter l’invitation et Refuser, Accepter l’invitation étant entouré en orange." width="624" height="308" /> <figcaption> Le membre d’exemple Jamie Chen, invité par Alex Morgan à la place de piscine Transat #4. </figcaption> </figure> Une fois que vous avez répondu, la page affiche **Invitation acceptée** ou **Invitation refusée**. Si vous avez un compte membre dans ce club, l’invitation apparaît aussi dans vos [réservations](/fr/guide/members/tickets-stays-and-people#5-les-r%C3%A9servations-auxquelles-vous-%C3%AAtes-invit%C3%A9), où vous pouvez également y répondre. Le lien fonctionne pendant 7 jours, et jamais après la fin de la réservation. Quand il a expiré, ou que la réservation a été annulée, la page indique **Invitation indisponible**. <figure> <img src="/images/guide/fr/members/reservation-invitation-unavailable.png" alt="Carte Invitation indisponible indiquant que l’invitation a expiré ou que la réservation n’est plus disponible, avec un bouton Retour à l’accueil." width="624" height="176" /> <figcaption> Un lien d’invitation à une réservation qui ne fonctionne plus. </figcaption> </figure> ## 3. Se désabonner d’une lettre d’information [#3-se-désabonner-dune-lettre-dinformation] En bas de chaque lettre d’information du club, sélectionnez **Se désabonner / Unsubscribe**. La page demande **Se désabonner de la lettre d’information ?** : sélectionnez **Se désabonner** pour confirmer. <figure> <img src="/images/guide/fr/members/newsletter-unsubscribe.png" alt="Carte Se désabonner de la lettre d’information ? avec un bouton Se désabonner entouré en orange." width="400" height="160" /> <figcaption> La page ouverte depuis le lien de désabonnement d’une lettre d’information. </figcaption> </figure> La page indique ensuite **Vous êtes désabonné** et nomme le club dont vous ne recevez plus les lettres d’information. Si elle indique **Ce lien de désabonnement n’est pas valide**, ouvrez de nouveau le lien complet depuis l’e-mail de la lettre d’information. Pour gérer la lettre d’information depuis votre espace membre, consultez [Notifications](/fr/guide/members/notifications). > Pour inviter vous-même une personne, consultez [Invités et > parrainage](/fr/guide/members/guests-and-sponsorship). Pour inviter quelqu’un à votre réservation, > consultez [Inviter les personnes qui vous > accompagnent](/fr/guide/members/tickets-stays-and-people#4-inviter-les-personnes-qui-vous-accompagnent). # Cours avec un coach Suivre une demande de cours, accepter l’horaire proposé par le club et consulter le profil de votre coach. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/lessons-with-a-coach Les cours avec un coach, comme les cours particuliers, commencent par une demande sans horaire fixé. Vous convenez de l’horaire avec le club et le coach dans la conversation de la réservation, puis vous acceptez l’horaire qu’ils proposent. Pour envoyer une demande de cours, consultez [Demandes et listes d’attente](/fr/guide/members/requests-and-waitlists). ## 1. Suivre une demande de coaching [#1-suivre-une-demande-de-coaching] Ouvrez **Mon compte**, sélectionnez l’onglet **Réservations** et sélectionnez le cours dans **Mes réservations à venir**. Tant que le club n’a pas fixé d’horaire, la réservation affiche **En attente du club** et son **Heure** indique **À convenir**. Utilisez la carte **Conversation** en bas de la page pour indiquer vos disponibilités au club et au coach : 1. Saisissez votre message dans la zone **Écrivez un message…**. 2. Sélectionnez la flèche d’envoi, ou appuyez sur <kbd>Entrée</kbd>. Appuyez sur <kbd>Maj</kbd> + <kbd>Entrée</kbd> pour aller à la ligne. <figure> <img src="/images/guide/fr/members/coaching-request.png" alt="Demande de Cours particulier pour Yoga with Thomas avec le statut En attente du club, l’heure À convenir, et une conversation où Alex Morgan a demandé un cours et tape une réponse, avec la zone de message et la flèche d’envoi entourées en orange." width="1280" height="800" /> <figcaption> La demande de cours de yoga d’exemple d’Alex Morgan, avec une réponse en cours de saisie. Les commandes à utiliser sont entourées en orange. </figcaption> </figure> ## 2. Accepter un horaire proposé [#2-accepter-un-horaire-proposé] Quand le club propose un horaire, la liste de vos réservations affiche **Proposé :** suivi de cet horaire. La réservation affiche **Proposition à accepter** et une carte **Horaire proposé** avec la date, le **Total** et la suite : * **Confirmée dès acceptation.** Le cours est confirmé dès que vous acceptez. * **Paiement dû après acceptation.** La réservation reste alors **Réservée — paiement dû** jusqu’à ce que vous la payiez. Consultez [Payer une réservation](/fr/guide/members/reservations-and-coaching#3-payer-une-r%C3%A9servation). La mention **Gratuit** signifie que le club vous offre la séance. Sélectionnez **Accepter l’horaire proposé** pour la réserver. Dans **Mes réservations à venir**, une réservation avec une proposition en attente affiche **Voir la proposition**. <figure> <img src="/images/guide/fr/members/coaching-proposal.png" alt="Réservation de Cours particulier pour Tennis with Julien avec le statut Proposition à accepter, une carte Horaire proposé marquée Gratuit pour un jour à venir de 18:00 à 19:00, un total de 0 Stars, Confirmée dès acceptation, un bouton Accepter l’horaire proposé entouré en orange, et la conversation avec Sport coach et Voir le profil." width="1280" height="800" /> <figcaption> Un premier cours de tennis gratuit proposé au membre d’exemple Alex Morgan. </figcaption> </figure> Si l’horaire ne vous convient pas, écrivez au club dans la conversation. Quand le club déplace une réservation qui avait déjà un horaire, la carte indique **Nouvel horaire proposé** et propose **Garder mon créneau actuel**. Une proposition dépassée affiche **Proposition expirée** : sélectionnez **Demander un nouveau créneau** pour écrire au club. ## 3. Consulter le profil de votre coach [#3-consulter-le-profil-de-votre-coach] Quand un coach répond depuis son propre compte, son nom apparaît en haut de la conversation avec **Voir le profil**. Ce lien ouvre sa biographie et ses diplômes, avec la date d’expiration de chacun. Un diplôme dont la date est passée affiche **Expiré**. Sélectionnez **Retour à la conversation** pour revenir. <figure> <img src="/images/guide/fr/members/coach-profile.png" alt="Page de profil du coach Sport coach avec une biographie et trois diplômes : First aid certificate (PSC1) de la Croix-Rouge française, Pool lifeguard certificate (BNSSA) marqué Expiré, et Sports instructor professional card, chacun avec ses dates de délivrance et d’expiration, et un bouton Retour à la conversation entouré en orange." width="1280" height="800" /> <figcaption> Le profil du coach d’exemple Sport coach, dont le diplôme de surveillant de baignade a expiré. </figcaption> </figure> # Votre adhésion Consulter votre adhésion et choisir un plan. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/membership-and-credits Votre adhésion vous donne accès à votre club. Les [crédits](/fr/guide#credits) sont la monnaie propre au club pour les réservations : chaque club leur donne son propre nom, par exemple Stars ⭐ chez Ukab Clubs. Les deux se trouvent dans l’onglet **Facturation** de **Mon compte**. ## 1. Consulter votre adhésion [#1-consulter-votre-adhésion] Ouvrez **Mon compte**, sélectionnez l’onglet **Facturation**, puis **Adhésion**. **Mes abonnements** affiche votre [plan](/fr/guide#plan) avec son statut et sa date de fin. <figure> <img src="/images/guide/fr/members/membership.png" alt="Page Adhésion de Jamie Chen affichant une Adhésion valide et un plan Payée, Adhésion 51 IÉNA (2026) - Abonnement Membre 2026, au prix de 1 800 € par an et valable un an, avec un bouton Modifier le moyen de paiement entouré en orange." width="1280" height="800" /> <figcaption> L’adhésion annuelle du membre d’exemple Jamie Chen. La commande à utiliser est entourée en orange. </figcaption> </figure> | Mention | Sens | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **Adhésion valide** | Votre adhésion est active. **Valide jusqu’au** donne sa date de fin. | | **Adhésion expirée** | Votre adhésion a pris fin. **Expirée le** donne la date. | | **Payée**, **Impayée** | Pour un plan qui se renouvelle : **Payée** tant que la période en cours dure, **Impayée** une fois sa date de fin passée sans renouvellement. | Les plans se renouvellent automatiquement à la fin de chaque période. Pour payer avec une autre carte, sélectionnez **Modifier le moyen de paiement**. Vos réglages de paiement s’ouvrent sur une page Stripe sécurisée, où vous mettez à jour votre carte avant de revenir à votre compte. Pour arrêter votre plan, contactez votre club : la page de paiement permet seulement de changer de carte. Votre adhésion n’est prolongée que lorsqu’un paiement de renouvellement réussit. Si votre carte est refusée, mettez-la à jour avec **Modifier le moyen de paiement** avant la date de fin, sinon votre adhésion prend fin à cette date. Quand votre club a enregistré votre adhésion lui-même, sans paiement en ligne, la page n’a pas de bouton **Modifier le moyen de paiement**. Demandez à votre club comment la renouveler. <figure> <img src="/images/guide/fr/members/membership-club.png" alt="Carte Mes abonnements d’Alex Morgan avec une Adhésion valide et un plan Payée, Adhésion 51 IÉNA (2026) - Abonnement Membre 2026, au prix de 1 800 € par an et valable jusqu’au 31 décembre 2026, sans bouton Modifier le moyen de paiement." width="1056" height="248" /> <figcaption> L’adhésion d’Alex Morgan, enregistrée par Ukab Clubs. </figcaption> </figure> Si votre adhésion a expiré alors qu’un abonnement est encore lié à votre compte, la page affiche **Votre adhésion est à régulariser**. Sélectionnez **Voir mes factures** pour trouver ce qui reste à payer, ou contactez votre club avant de vous réabonner. > L’[espace membre](/fr/guide#member-app) n’a pas de bouton pour changer de plan. Demandez à votre club > : l’équipe met à jour votre adhésion pour vous. ## 2. Choisir une adhésion [#2-choisir-une-adhésion] Sans adhésion, la page affiche **Aucune adhésion pour le moment** et les plans proposés par votre club. 1. Sous **Choisissez votre adhésion**, sélectionnez un plan. Chaque plan indique son prix et sa période. 2. Sélectionnez **Payer … avec Stripe**. Le bouton reprend le prix du plan sélectionné. 3. Payez sur la page Stripe sécurisée. Vous revenez ensuite à votre compte. <figure> <img src="/images/guide/fr/members/membership-plans.png" alt="Liste Choisissez votre adhésion avec trois plans à 1 800 €, 4 500 € et 6 300 € par an, le plan Abonnement Cercle sélectionné et un bouton Payer 4 500 € / an avec Stripe entouré en orange." width="1056" height="396" /> <figcaption> Les trois plans d’exemple d’Ukab Clubs, vus par Louis Boulanger. </figcaption> </figure> Pendant la vérification du paiement, la page affiche **Confirmation de votre paiement** : attendez qu’elle se mette à jour et ne payez pas une seconde fois. **Paiement confirmé** signifie que votre adhésion est enregistrée. ### Si le paiement n’aboutit pas [#si-le-paiement-naboutit-pas] | Message | Que faire | | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Le paiement a été annulé. Aucune adhésion Stripe n’a été confirmée. Vous pouvez réessayer.** | Vous avez quitté la page Stripe sans payer. Choisissez un plan et sélectionnez de nouveau **Payer … avec Stripe**. | | **La session de paiement a expiré. Vous pouvez lancer une nouvelle tentative.** | La page de paiement est restée ouverte trop longtemps. Sélectionnez **Lancer un nouveau paiement**. | | **Session de paiement manquante. Ouvrez le lien de retour Stripe d’origine pour vérifier ce paiement.** | Le lien de retour a perdu sa référence de paiement. Ouvrez le lien d’origine de Stripe pour vérifier le paiement. | | **Réessayer le paiement** sur le bouton | La page de paiement n’a pas pu s’ouvrir. Sélectionnez le bouton pour réessayer. | | **Le paiement en ligne est actuellement indisponible. Veuillez contacter votre club.** | Le club ne prend pas encore les paiements en ligne. Contactez-le pour adhérer. | <figure> <img src="/images/guide/fr/members/membership-cancelled.png" alt="Mes abonnements avec Aucune adhésion pour le moment, le message Le paiement a été annulé. Aucune adhésion Stripe n’a été confirmée. Vous pouvez réessayer., les trois plans et un bouton Payer 1 800 € / an avec Stripe entouré en orange." width="1056" height="432" /> <figcaption> Louis Boulanger, revenu de la page Stripe sans avoir payé. </figcaption> </figure> Pour recharger vos crédits et suivre vos dépenses, consultez [Vos crédits](/fr/guide/members/credits). # Messages Écrire à votre club, lire ses réponses et commencer une nouvelle conversation. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/messages Votre [espace membre](/fr/guide#member-app) comprend une messagerie privée avec l’équipe de votre club. Utilisez-la pour toute question sur le club, comme un casier, le parking ou un évènement. Les questions sur un cours précis se posent dans la conversation de cette réservation : consultez [Cours avec un coach](/fr/guide/members/lessons-with-a-coach). ## 1. Ouvrir vos messages [#1-ouvrir-vos-messages] Dans votre espace membre, sélectionnez l’icône **Boîte de réception** à côté de **Se déconnecter**. La **Boîte de réception** s’ouvre sur **Messages**. Le nombre affiché sur **Messages** indique combien de conversations ont une réponse que vous n’avez pas encore lue. Sélectionnez la flèche à côté de **Boîte de réception** pour revenir à votre compte. Chaque conversation affiche son objet, le dernier message et sa date d’envoi. Vos propres messages commencent par **Vous :**, et ceux du club par le nom de la personne qui a écrit. Un point à côté de l’objet signale une conversation avec une réponse non lue. <figure> <img src="/images/guide/fr/members/chats.png" alt="Onglet Messages de la boîte de réception d’Alex Morgan listant deux conversations non lues, Locker for the season et Parking during the tournament, chacune avec un bouton Ouvrir, le second étant entouré en orange, et un bouton Nouvelle conversation." width="1280" height="800" /> <figcaption> Les conversations du membre d’exemple Alex Morgan avec Ukab Clubs. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 2. Lire une conversation et y répondre [#2-lire-une-conversation-et-y-répondre] Sélectionnez **Ouvrir** sur une conversation. Les messages du club s’affichent à gauche avec le nom de l’expéditeur, les vôtres à droite, chacun avec sa date et son heure. Ouvrir la conversation la marque comme lue. 1. Saisissez votre réponse dans la zone **Écrivez un message…** en bas. 2. Sélectionnez la flèche d’envoi, ou appuyez sur <kbd>Entrée</kbd>. Appuyez sur <kbd>Maj</kbd> + <kbd>Entrée</kbd> pour aller à la ligne sans envoyer. <figure> <img src="/images/guide/fr/members/chat-conversation.png" alt="Conversation Parking during the tournament avec un message de Club team sur la fermeture du parking samedi, et une réponse d’Alex Morgan en cours de saisie dans la zone de message, entourée en orange avec sa flèche d’envoi." width="1280" height="800" /> <figcaption> Une réponse en cours de rédaction dans la conversation d’exemple Parking during the tournament. </figcaption> </figure> Sélectionnez **Conversations** au-dessus de la conversation pour revenir à la liste. ## 3. Commencer une nouvelle conversation [#3-commencer-une-nouvelle-conversation] 1. Sur **Messages**, sélectionnez **Nouvelle conversation**. 2. Saisissez un **Objet** qui résume votre question. 3. Rédigez votre **Message**. 4. Sélectionnez **Envoyer**. La nouvelle conversation s’ouvre, et l’équipe du club la voit dans sa boîte de réception. Sélectionnez **Annuler** pour fermer le formulaire sans envoyer. <figure> <img src="/images/guide/fr/members/new-chat.png" alt="Formulaire Nouvelle conversation avec l’objet Guest towels et un message demandant à emprunter deux serviettes de plus samedi, avec des boutons Annuler et Envoyer, Envoyer étant entouré en orange." width="1072" height="316" /> <figcaption> Une question d’exemple pour le club, prête à être envoyée. </figcaption> </figure> > Le club répond depuis son [back-office](/fr/guide#back-office), comme le décrit [Répondre aux > messages](/fr/guide/staff/chats). Les réponses d’un compte de l’équipe sans nom sont signées **Club > team**. Selon vos [réglages de notifications](/fr/guide/members/notifications), vous recevez aussi > une alerte ou un e-mail quand une réponse arrive. # Notifications Lire les alertes de votre club et choisir celles que vous recevez dans l’application ou par e-mail. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/notifications Votre club vous envoie des notifications sur vos réservations, votre adhésion et vos messages, par exemple un rappel la veille d’une réservation. Vous les lisez dans votre [espace membre](/fr/guide#member-app) et vous choisissez, pour chaque type d’alerte, si elle vous parvient aussi par e-mail. ## 1. Ouvrir vos notifications [#1-ouvrir-vos-notifications] Dans votre espace membre, sélectionnez l’icône **Boîte de réception** à côté de **Se déconnecter**, puis sélectionnez l’onglet **Notifications**. Le nombre affiché sur l’icône **Boîte de réception** compte ensemble vos notifications et vos conversations non lues. Chaque notification affiche un titre, un court texte, la réservation ou l’élément concerné, et son heure d’arrivée. Un lien vous mène à la bonne page, comme **Voir la réservation**. Les notifications non lues ont un point et un titre en gras. <figure> <img src="/images/guide/fr/members/notifications.png" alt="Onglet Notifications, entouré en orange, de la boîte de réception de Jamie Chen avec des notifications non lues, montrant deux rappels de réservation à venir pour Le Restaurant et Le Rooftop, chacun avec un lien Voir la réservation." width="1280" height="800" /> <figcaption> Les rappels de réservation du membre d’exemple Jamie Chen chez Ukab Clubs. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 2. Trier les notifications et les marquer comme lues [#2-trier-les-notifications-et-les-marquer-comme-lues] * Sélectionnez **Non lues** pour ne voir que les nouvelles, et **Toutes** pour tout revoir. * Sélectionnez l’icône d’enveloppe d’une notification : elle sert à **Marquer comme lu**, ou à **Marquer comme non lu** si vous voulez y revenir. * Sélectionnez **Tout marquer comme lu** pour effacer d’un coup toutes les notifications non lues. Un message propose **Annuler** pendant quelques secondes. Utilisez **Précédent** et **Suivant** en bas de la liste quand vous avez beaucoup de notifications. Ouvrir une conversation dans [Messages](/fr/guide/members/messages) marque aussi ses alertes de message comme lues. ## 3. Choisir ce que vous recevez [#3-choisir-ce-que-vous-recevez] Sélectionnez l’icône d’engrenage à côté de **Tout marquer comme lu** pour ouvrir **Réglages des notifications**. Chaque ligne correspond à un type d’alerte avec deux interrupteurs : * **Dans l’application** affiche l’alerte dans vos notifications, * **E-mail** l’envoie à votre adresse e-mail. Activez ou désactivez un interrupteur. Comme l’indique la fenêtre, **Les modifications sont enregistrées automatiquement.** Elle affiche **Préférences enregistrées** une fois que c’est fait. Fermez la fenêtre avec la croix. <figure> <img src="/images/guide/fr/members/notification-settings.png" alt="Haut de la fenêtre Réglages des notifications, avec la note Les modifications sont enregistrées automatiquement et les premières alertes, Changements de réservation, Nouvelles propositions de date, Arrivée des membres parrainés et Arrivée des invités, chacune avec des interrupteurs Dans l’application et E-mail activés." width="448" height="337" /> <figcaption> Le haut des réglages des notifications du membre d’exemple Jamie Chen, tous activés. Le tableau ci-dessous liste toutes les alertes. </figcaption> </figure> | Alerte | Quand elle arrive | | ---------------------------------- | -------------------------------------------------------------------------------------------------- | | **Changements de réservation** | Une réservation change de statut, par exemple confirmée ou annulée, ou une caution est remboursée. | | **Nouvelles propositions de date** | Le club propose un horaire pour un cours ou une réservation. | | **Arrivée des membres parrainés** | Une personne que vous avez parrainée rejoint le club. | | **Arrivée des invités** | Une personne que vous avez invitée rejoint le club ou accepte votre invitation. | | **Rappels de réservation** | 24 heures avant votre réservation. | | **Liste d’attente** | Une place en liste d’attente devient une réservation pour vous. | | **Renouvellement de l’adhésion** | 7 jours avant la fin de votre période d’adhésion. | | **Annulations de séances** | Le club annule une séance à laquelle vous êtes inscrit. | | **Messages de réservation** | Un nouveau message arrive dans la conversation d’une réservation. | | **Messages des conversations** | Le club répond dans [Messages](/fr/guide/members/messages). | | **Messages du club** | Le club vous écrit, par exemple pour un colis à l’accueil. | | **Solde de crédits bas** | Votre solde de crédits devient faible. | La dernière ligne, **Lettre d’information du club**, n’a qu’un interrupteur **E-mail**. Désactivez-le pour ne plus recevoir la lettre d’information de votre club. ## 4. Se désabonner de la lettre d’information [#4-se-désabonner-de-la-lettre-dinformation] Vous pouvez arrêter la lettre d’information du club de deux façons : * Depuis votre espace membre : désactivez l’interrupteur **E-mail** de **Lettre d’information du club** dans **Réglages des notifications**. * Depuis un e-mail de la lettre d’information : sélectionnez **Se désabonner / Unsubscribe** en bas de l’e-mail, puis **Se désabonner** sur la page qui s’ouvre. La page confirme **Vous êtes désabonné**. Certaines messageries affichent aussi leur propre bouton de désabonnement, qui fonctionne de la même façon. Les deux façons modifient le même réglage : après un désabonnement depuis un e-mail, l’interrupteur **Lettre d’information du club** est désactivé dans vos réglages des notifications. Réactivez-le pour recevoir de nouveau la lettre d’information. Pour la page de désabonnement et ses messages, consultez [Accepter une invitation reçue par e-mail](/fr/guide/members/invitations-from-email). # Se connecter, activer votre compte et réinitialiser votre mot de passe Vous connecter à votre espace membre, choisir votre mot de passe la première fois et retrouver l’accès. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/passwords-and-activation Vous vous connectez à votre [espace membre](/fr/guide#member-app) avec votre adresse e-mail et un mot de passe que vous choisissez. Vous définissez ce mot de passe une fois, à la création de votre compte, et vous pouvez le remplacer à tout moment depuis la page de connexion. ## 1. Se connecter [#1-se-connecter] Sélectionnez **Mon compte** dans la navigation en haut de l’espace membre. Quand vous n’êtes pas connecté, la page **Bon retour** s’ouvre. 1. Dans **Club**, choisissez votre club. La page retient le dernier club auquel vous vous êtes connecté. 2. Saisissez votre **E-mail** et votre **Mot de passe**. Sélectionnez l’icône en forme d’œil pour afficher ou masquer le mot de passe. 3. Sélectionnez **Se connecter**. Votre espace membre s’ouvre. <figure> <img src="/images/guide/fr/members/sign-in.png" alt="Formulaire de connexion Bon retour avec un champ Club vide invitant à choisir un club, entouré en orange, l’e-mail alex@example.com, un champ Mot de passe vide avec un lien Mot de passe oublié ?, un bouton Se connecter et la ligne Pas encore membre ? Candidater à un club." width="400" height="425" /> <figcaption> Le formulaire de connexion avec l’e-mail du membre d’exemple, avant le choix du club. Le champ à utiliser est entouré en orange. </figcaption> </figure> **Se connecter** devient disponible une fois le club, l’e-mail et le mot de passe remplis. Pas encore membre ? Sélectionnez **Candidater à un club** pour trouver un club et candidater : consultez [Trouver un club et candidater](/fr/guide/members/find-a-club-and-apply). Quand votre session expire, ou que votre club vous déconnecte, la page affiche **Votre session a expiré. Veuillez vous reconnecter.** Reconnectez-vous pour continuer. ## 2. Choisir votre premier mot de passe [#2-choisir-votre-premier-mot-de-passe] La façon de choisir votre premier mot de passe dépend de la façon dont vous avez rejoint le club. | Comment vous avez rejoint le club | Où vous choisissez votre mot de passe | | ------------------------------------------------------ | -------------------------------------------------------------------------------- | | Vous avez candidaté en ligne et le club vous a accepté | Sur votre candidature, une fois toutes les étapes après l’acceptation terminées. | | Le club a créé votre compte | Depuis le lien d’activation de l’e-mail que le club vous a envoyé. | ### Après une candidature acceptée [#après-une-candidature-acceptée] Quand votre [candidature](/fr/guide#application) est complète, elle affiche **Choisissez votre mot de passe**. Saisissez votre mot de passe dans **Nouveau mot de passe** puis de nouveau dans **Confirmer le mot de passe**, et sélectionnez **Enregistrer le mot de passe et ouvrir mon compte**. Votre espace membre s’ouvre aussitôt. Pour revenir à votre candidature, consultez [Remplir et signer votre candidature](/fr/guide/members/sign-your-application). Sur le site de candidature propre au club, le bouton s’intitule **Enregistrer le mot de passe et me connecter**. Il ouvre la page de connexion de l’espace membre de votre club : connectez-vous avec votre e-mail et le mot de passe que vous venez de choisir. ### Depuis un e-mail d’activation [#depuis-un-e-mail-dactivation] Quand le club vous ajoute lui-même, vous recevez un e-mail, rédigé en anglais, intitulé « Activate your … account », avec le nom du club à la place des points de suspension. Ouvrez son lien dans les 7 jours. 1. Saisissez votre mot de passe dans **Mot de passe** puis de nouveau dans **Confirmer le mot de passe**. 2. Sélectionnez **Activer mon compte**. 3. Quand la page indique que votre compte est activé, sélectionnez **Retour à la connexion** et connectez-vous avec votre e-mail et votre nouveau mot de passe. <figure> <img src="/images/guide/fr/members/activate-account.png" alt="Carte Activez votre compte avec les champs Mot de passe et Confirmer le mot de passe remplis, la règle du mot de passe, un bouton Activer mon compte entouré en orange et un lien Retour à la connexion en bas." width="400" height="385" /> <figcaption> Le formulaire d’activation, rempli avec un mot de passe d’exemple. </figcaption> </figure> Si la page indique **Lien d’activation incomplet**, ouvrez de nouveau le lien complet depuis l’e-mail. Si le lien ne fonctionne plus, sélectionnez **Demander un nouveau lien** pour choisir votre mot de passe avec un lien de réinitialisation, ou demandez au club de vous renvoyer un e-mail d’activation. > Utilisez au moins 12 caractères, dont une minuscule, une majuscule, un chiffre et un symbole. Le > formulaire vous prévient quand le mot de passe est trop faible ou quand les deux mots de passe ne > correspondent pas. ## 3. Mot de passe oublié [#3-mot-de-passe-oublié] 1. Sur la page de connexion, sélectionnez **Mot de passe oublié ?**. 2. Vérifiez le **Club** et saisissez votre **E-mail**. 3. Sélectionnez **Envoyer le lien de réinitialisation**. <figure> <img src="/images/guide/fr/members/forgot-password.png" alt="Formulaire Mot de passe oublié avec Ukab Clubs dans le champ Club, une adresse e-mail d’exemple, un bouton Envoyer le lien de réinitialisation entouré en orange et un lien Retour à la connexion." width="400" height="349" /> <figcaption> Demande d’un lien de réinitialisation pour une adresse e-mail d’exemple chez Ukab Clubs. </figcaption> </figure> La page confirme ensuite que vous recevrez un lien de réinitialisation si un compte correspond. Pour votre sécurité, elle affiche le même message que l’adresse ait un compte ou non. L’e-mail, rédigé en anglais et intitulé « Reset your Champagne password », contient un lien valable 30 minutes. ## 4. Choisir un nouveau mot de passe [#4-choisir-un-nouveau-mot-de-passe] Ouvrez le lien de l’e-mail. La page **Choisissez un nouveau mot de passe** s’ouvre. 1. Saisissez votre mot de passe dans **Nouveau mot de passe** puis de nouveau dans **Confirmer le mot de passe**. 2. Sélectionnez **Enregistrer le mot de passe**. 3. Quand la page confirme le changement, sélectionnez **Retour à la connexion** et connectez-vous avec votre nouveau mot de passe. <figure> <img src="/images/guide/fr/members/reset-password.png" alt="Formulaire Choisissez un nouveau mot de passe avec les champs Nouveau mot de passe et Confirmer le mot de passe remplis, la règle du mot de passe, un bouton Enregistrer le mot de passe entouré en orange et un lien Retour à la connexion." width="400" height="409" /> <figcaption> Le formulaire de réinitialisation, rempli avec un mot de passe d’exemple. </figcaption> </figure> Si la page indique **Lien de réinitialisation incomplet**, ouvrez de nouveau le lien complet depuis l’e-mail, ou sélectionnez **Demander un nouveau lien** pour recommencer. Si le lien a expiré, demandez-en un nouveau depuis **Mot de passe oublié ?**. # Demander une réservation ou rejoindre une liste d’attente S’inscrire à des séances collectives, demander un cours à votre club et attendre une place quand un horaire est complet. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/requests-and-waitlists Toutes les activités ne se réservent pas de la même façon. Les cours collectifs ont des séances auxquelles vous vous inscrivez, les cours particuliers demandent au club de convenir d’un horaire avec vous, et les horaires très demandés se remplissent vite. Depuis le **Planning du club** de votre [espace membre](/fr/guide#member-app), vous vous inscrivez, envoyez une demande ou rejoignez une liste d’attente. Pour une réservation classique dont vous choisissez l’horaire, consultez [Faire une réservation](/fr/guide/members/book-a-reservation). ## 1. Lire les séances collectives [#1-lire-les-séances-collectives] Ouvrez **Mon compte**, sélectionnez l’onglet **Réservations** et faites défiler jusqu’à **Planning du club**. Avec **Toutes les activités programmées** sélectionné dans **Activité**, le planning liste les séances de la semaine de toutes les activités, jour par jour. Choisissez une activité dans **Activité** pour ne voir que ses séances, et utilisez les flèches ou **Cette semaine** pour passer d’une semaine à l’autre. <figure> <img src="/images/guide/fr/members/group-sessions.png" alt="Planning du club avec Toutes les activités programmées sélectionné, montrant une semaine de cours Cours collectif marqués Sur inscription, un Open studio quotidien à 12:30 marqué Accès libre et un Concert — matinée le samedi marqué Déjà réservé." width="1056" height="734" /> <figcaption> Une semaine chez Ukab Clubs : les cours Cours collectif, l’Open studio quotidien et le concert d’Alex Morgan. </figcaption> </figure> Chaque séance affiche son horaire, son nom et une mention : | Mention | Ce que cela signifie | | ------------------- | -------------------------------------------------------------------------------------- | | **Sur inscription** | Vous vous inscrivez pour participer. Sélectionnez la séance pour réserver votre place. | | **Accès libre** | Pas d’inscription : venez à l’heure indiquée. | | **Déjà réservé** | Vous avez une place. Sélectionnez la séance pour ouvrir votre réservation. | | **Demande envoyée** | Vous avez demandé une place et le club n’a pas encore répondu. | Sélectionnez une séance **Accès libre** pour voir sa date, son horaire et son lieu. Sélectionnez **Ajouter à mon calendrier** pour l’enregistrer dans votre application d’agenda. <figure> <img src="/images/guide/fr/members/open-access-session.png" alt="Fenêtre Open studio avec la date et l’horaire de 12:30 à 13:15, Main studio, la mention Accès libre et un bouton Ajouter à mon calendrier entouré en orange." width="384" height="262" /> <figcaption> La séance d’exemple Open studio, ouverte à tous les membres sans inscription. La commande à utiliser est entourée en orange. </figcaption> </figure> Quand la semaine n’a aucune séance, le planning indique **Aucune séance cette semaine.** Sélectionnez **Voir les prochaines séances** pour passer à la prochaine semaine qui en a une. ## 2. S’inscrire à une séance collective [#2-sinscrire-à-une-séance-collective] Sélectionnez une séance marquée **Sur inscription**. Un formulaire s’ouvre avec le lieu, la date et l’horaire, et le nombre de places restantes. Le **Total** affiche le prix. <figure> <img src="/images/guide/fr/members/session-registration.png" alt="Formulaire Cours collectif indiquant Main studio, une date de 15:00 à 16:00 avec 6 places restantes, un total Gratuit et un bouton Réserver entouré en orange." width="448" height="286" /> <figcaption> Inscription à un cours d’exemple Cours collectif qui a encore 6 places. </figcaption> </figure> Sélectionnez **Réserver**. Le formulaire confirme avec l’un de ces messages : | Message | La suite | | --------------------------------------------------------------------------- | ----------------------------------------------------------------- | | **Réservation confirmée.** | Votre place est réservée. | | **Réservation effectuée. Payez depuis vos réservations pour la confirmer.** | Sélectionnez **Payer la réservation** pour payer et la confirmer. | | **Demande envoyée, en attente de validation du club.** | Le club vérifie votre demande avant de la confirmer. | Sélectionnez **Voir la réservation**, ou **Payer la réservation** quand un paiement est dû, pour ouvrir la réservation. Sélectionnez **Fermer** pour revenir au planning. ## 3. Demander une réservation [#3-demander-une-réservation] Pour certaines activités, comme les cours particuliers, le club convient de l’horaire avec vous. Choisissez l’activité dans **Activité** : le planning affiche un bouton **Demander une réservation**. 1. Sélectionnez **Demander une réservation**. 2. Dans **Ressource**, choisissez le cours ou le coach voulu. Le formulaire indique **Horaire à convenir avec votre club.** 3. Vérifiez le **Total** et votre **Solde disponible**. **Sous réserve de validation du club** vous rappelle que rien n’est définitif tant que le club n’a pas répondu. 4. Sélectionnez **Demander une réservation**. <figure> <img src="/images/guide/fr/members/request-reservation.png" alt="Formulaire de demande Cours particulier avec Pilates with Sarah sélectionné parmi cinq cours à 80 Stars, la mention Horaire à convenir avec votre club, un Total de 80 Stars, un Solde disponible de 540 Stars, Sous réserve de validation du club et un bouton Demander une réservation entouré en orange." width="448" height="524" /> <figcaption> Une demande pour le cours d’exemple Pilates with Sarah, payé en Stars. </figcaption> </figure> Le formulaire indique **Demande envoyée, en attente de validation du club.** Votre demande apparaît dans **Mes réservations à venir** avec le statut **En attente du club**. Le club propose ensuite un horaire dans la conversation de la réservation : consultez [Cours avec un coach](/fr/guide/members/lessons-with-a-coach) pour la suivre et accepter l’horaire. ## 4. Rejoindre une liste d’attente [#4-rejoindre-une-liste-dattente] Quand une séance ou un horaire est complet, le club peut vous permettre d’attendre une place. * Une séance complète affiche la mention « complet » à côté de sa date. Le formulaire indique **Liste d’attente · aucun débit**. * Un horaire complet, par exemple dans un restaurant, est barré parmi les horaires. Sélectionnez-le : le formulaire indique **Cet horaire est complet. Rejoignez la liste d’attente pour être réservé si une place se libère.** Quand une activité liée a encore de la place, un bouton comme **Disponible à …** vous permet de basculer vers elle. <figure> <img src="/images/guide/fr/members/session-waitlist.png" alt="Formulaire Cours collectif d’un cours complet de 15:00 à 16:00, un total Gratuit, la mention Liste d’attente · aucun débit et un bouton Rejoindre la liste d’attente entouré en orange." width="448" height="318" /> <figcaption> Un cours d’exemple Cours collectif complet, avec sa liste d’attente. </figcaption> </figure> <figure> <img src="/images/guide/fr/members/join-waitlist.png" alt="Formulaire de réservation Le Restaurant pour 8 personnes : 12:00 et 12:30 sont libres, 19:00 à 21:00 sont barrés, 19:00 est sélectionné avec le message Cet horaire est complet. Rejoignez la liste d’attente pour être réservé si une place se libère, et un bouton Rejoindre la liste d’attente entouré en orange." width="1280" height="800" /> <figcaption> Le Restaurant d’exemple, complet le soir pour une table de huit. </figcaption> </figure> Sélectionnez **Rejoindre la liste d’attente**. Le formulaire affiche votre rang, comme **Liste d’attente · n° 1**, avec un bouton **Quitter**. Votre place en attente apparaît aussi dans **Mes réservations à venir** avec son rang. <figure> <img src="/images/guide/fr/members/session-waitlist-place.png" alt="Formulaire Cours collectif d’un cours complet de 09:00 à 10:00 au Main studio, un total Gratuit, la mention Liste d’attente · aucun débit, la mention Liste d’attente · n° 3 et un bouton Quitter entouré en orange." width="448" height="318" /> <figcaption> Alex Morgan, troisième sur la liste d’attente du cours complet de demain à 09:00. </figcaption> </figure> <figure> <img src="/images/guide/fr/members/waitlist-upcoming.png" alt="Ligne de Mes réservations à venir pour Cours collectif avec la mention Liste d’attente · n° 3, Main studio, l’horaire de 09:00 à 10:00 et un bouton Quitter entouré en orange." width="1008" height="116" /> <figcaption> La même place en attente dans les réservations à venir d’Alex Morgan. </figcaption> </figure> Quand une place se libère, Champagne la réserve pour vous, dans l’ordre d’arrivée sur la liste d’attente. Vous recevez une notification **Liste d’attente** : ouvrez la réservation pour vérifier son statut et un éventuel paiement dû. Pour ne plus attendre, sélectionnez **Quitter** à côté de votre place en liste d’attente, dans le formulaire ou dans **Mes réservations à venir**. # Vos réservations Ouvrir une réservation pour lire ses détails, la payer, la modifier ou l’annuler. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/reservations-and-coaching Une fois votre réservation faite, elle a sa propre page dans votre [espace membre](/fr/guide#member-app). Depuis cette page, vous voyez ce que coûte la réservation, vous la payez, la modifiez ou l’annulez. Pour faire une nouvelle réservation, consultez [Faire une réservation](/fr/guide/members/book-a-reservation). ## 1. Ouvrir une réservation [#1-ouvrir-une-réservation] Ouvrez **Mon compte**, sélectionnez l’onglet **Réservations** et sélectionnez une réservation dans **Mes réservations à venir**. Vous pouvez aussi sélectionner la carte **Prochaine réservation** de l’onglet **Aperçu**. La page de la réservation affiche l’activité, son statut et la salle, la place ou le coach réservé. La carte de détails commence par les horaires **Début** et **Fin** côte à côte, suivis du **Prix de la réservation**. Sélectionnez **Mes réservations** en haut pour revenir à la liste. <figure> <img src="/images/guide/fr/members/reservation-detail.png" alt="Réservation Suite d’hôtel dans la Courtyard suite avec le statut Réservée — paiement dû, les boutons Modifier et Annuler la réservation, une carte de détails avec les dates de début et de fin côte à côte en haut, le prix de 750 Stars, la ligne Annulation Prix non remboursable : le délai d’annulation est dépassé, l’avertissement Votre solde est de 540 Stars. Il vous manque 210 Stars, et un bouton Recharger mes crédits." width="1280" height="800" /> <figcaption> La réservation d’exemple Suite d’hôtel d’Alex Morgan, qui commence demain et attend son paiement. </figcaption> </figure> Chaque réservation affiche l’un de ces statuts : | Statut | Sens | | -------------------------- | -------------------------------------------------------------------------------- | | **En attente du club** | Votre demande est entre les mains du club, qui va fixer ou confirmer un horaire. | | **Proposition à accepter** | Le club a proposé un horaire. Acceptez-le pour confirmer votre cours. | | **Réservée — paiement dû** | Votre place est gardée et reste à payer. | | **Confirmée** | Votre réservation est réglée. | | **Terminé** | La réservation est passée. | | **Annulée** | La réservation a été annulée. | | **Proposition expirée** | L’horaire proposé par le club est passé sans réponse de votre part. | Les deux statuts de proposition concernent les cours avec un coach : consultez [Cours avec un coach](/fr/guide/members/lessons-with-a-coach). Quand une réservation concerne plusieurs personnes, la carte de détails affiche aussi le **Nombre de personnes**. Pour les séjours, les billets et les personnes qui vous accompagnent, consultez [Billets, séjours et personnes de votre réservation](/fr/guide/members/tickets-stays-and-people). ## 2. Lire les montants [#2-lire-les-montants] Selon le club et la réservation, la carte de détails affiche d’autres lignes sur les montants : | Ligne | Ce que cela signifie | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Caution** | Un montant que le club garde pendant votre réservation et vous rend normalement ensuite. | | **Total avec caution** | Le prix plus la caution : ce que vous payez. | | **Statut de la caution** | **À payer avec la réservation**, **Retenue jusqu’au remboursement**, **Remboursée**, ou la part remboursée et la part gardée par le club. | | **Message du club** | L’explication du club quand il règle votre caution, par exemple pourquoi il en a gardé une partie. | | **Annulation** | **Gratuite jusqu’au** suivi d’une date : annulez avant pour récupérer le prix. **Prix non remboursable : le délai d’annulation est dépassé** signifie que cette date est passée. | Si vous avez choisi des extras comme des boissons en réservant, ils sont listés sous **Produits facturés** en haut de la page, avec la quantité et le montant de chacun. Les produits que seul le club suit, comme un minibar, n’y figurent pas. Un séjour avec une caution et des produits facturés est présenté dans [Suivre votre séjour](/fr/guide/members/tickets-stays-and-people#3-suivre-votre-s%C3%A9jour). ## 3. Payer une réservation [#3-payer-une-réservation] Quand une réservation est **Réservée — paiement dû**, la carte de détails propose de la payer avec vos [crédits](/fr/guide#credits). * Si votre solde couvre le prix, sélectionnez **Payer** suivi du montant, par exemple **Payer 30 Stars ⭐**. Les crédits sont retirés de votre solde tout de suite. * Sinon, la carte affiche votre solde et le montant manquant, comme dans l’exemple Suite d’hôtel ci-dessus. Sélectionnez **Recharger mes crédits** pour en acheter, puis revenez payer. Consultez [Vos crédits](/fr/guide/members/credits). <figure> <img src="/images/guide/fr/members/reservation-pay.png" alt="Carte de détails d’une réservation Place à la piscine avec ses horaires de début et de fin en haut, un Prix de la réservation de 30 Stars et un bouton Payer 30 Stars entouré en orange." width="1056" height="236" /> <figcaption> Le solde de 540 Stars d’Alex Morgan couvre cette réservation d’exemple à la piscine, au Transat \#5. La commande à utiliser est entourée en orange. </figcaption> </figure> Une fois payée, la réservation devient **Confirmée**. Un compte temporaire créé en réservant en ligne paie par carte : sa réservation affiche **Payer … par carte**, qui ouvre une page de paiement sécurisée. À votre retour, un message indique si le paiement est passé. Consultez [Réserver en ligne en tant que visiteur](/fr/guide/members/book-online#4-payer-et-suivre-votre-r%C3%A9servation). <figure> <img src="/images/guide/fr/members/reservation-card-payment.png" alt="Carte de détails d’une réservation Suite d’hôtel avec ses dates de début et de fin en haut, un prix de 960,00 € pour 2 personnes, un Total avec caution de 1 010,00 €, une Caution de 50,00 €, le Statut de la caution À payer avec la réservation, la ligne Annulation avec la date limite d’annulation gratuite, et un bouton Payer 1 010,00 € par carte entouré en orange." width="1056" height="348" /> <figcaption> Le séjour au Penthouse réservé en ligne par le visiteur Marc Lefort, encore annulable gratuitement. </figcaption> </figure> ## 4. Modifier ou annuler une réservation [#4-modifier-ou-annuler-une-réservation] Jusqu’au début de la réservation, vous pouvez la modifier ou l’annuler vous-même. * **Modifier** ouvre le formulaire de réservation pour choisir une autre ressource, une autre date ou un autre horaire. Sélectionnez **Enregistrer**. Quand le prix ne change pas, la modification est enregistrée tout de suite. Quand il change, un récapitulatif affiche le **Prix actuel**, le **Nouveau prix** et la différence : sélectionnez **Confirmer la modification** pour l’appliquer. Sur une réservation confirmée, le formulaire vous rappelle que la différence est débitée de votre solde de crédits ou remboursée sur celui-ci. * **Annuler la réservation** ouvre une confirmation. Elle indique si le prix sera remboursé et combien de crédits reviennent sur votre solde. Quand le délai d’annulation est dépassé, elle prévient que le prix ne sera pas remboursé. <figure> <img src="/images/guide/fr/members/cancel-reservation.png" alt="Fenêtre Annuler cette réservation ? indiquant que l’action annule la réservation et que 30 Stars seront restitués au solde de crédits, avec les boutons Conserver la réservation et Annuler la réservation, Annuler la réservation entouré en orange." width="448" height="188" /> <figcaption> La confirmation d’annulation de la réservation payée d’Alex Morgan à la piscine. </figcaption> </figure> Sélectionnez **Conserver la réservation** pour fermer la fenêtre sans rien changer, ou **Annuler la réservation** pour confirmer. Certaines activités permettent seulement l’annulation : leur page n’a pas de bouton **Modifier**. Une fois la réservation commencée, seul le club peut la modifier. Après une annulation, la page affiche **Réservation annulée** avec les crédits rendus à votre solde, ou indique que le prix n’a pas été remboursé si vous avez annulé après la date limite. # Remplir et signer votre candidature Remplir votre candidature, la signer et l’envoyer au club. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/sign-your-application Votre [candidature](/fr/guide#application) est un formulaire en plusieurs étapes, préparé par le club. Vous répondez étape par étape, puis vous relisez vos réponses et les envoyez. Le club accepte ensuite ou refuse votre candidature. Une fois acceptée, il peut vous rester quelques étapes à terminer, comme le paiement de votre [plan](/fr/guide#plan). Pour commencer une candidature, consultez [Trouver un club et candidater](/fr/guide/members/find-a-club-and-apply). ## 1. Remplir chaque étape [#1-remplir-chaque-étape] Le haut du formulaire indique où vous en êtes, par exemple **Étape 2 sur 3**. Répondez à l’étape, puis sélectionnez **Continuer**. Votre réponse est enregistrée et l’étape suivante s’ouvre. Sélectionnez **Retour** pour revenir à l’étape précédente. Pour aller directement à une étape déjà remplie, sélectionnez **Étapes** et choisissez-la dans la liste. Les étapes que vous n’avez pas encore atteintes sont grisées. <figure> <img src="/images/guide/fr/members/application-steps.png" alt="Candidature de Nina Demo pour rejoindre Ukab Clubs à l’étape 2 sur 3, Vérifiez votre adresse e-mail, avec le bouton Étapes entouré en orange et son menu ouvert sur Votre nom, Vérifiez votre adresse e-mail, Présentez-vous et Vérifier." width="1280" height="800" /> <figcaption> La candidature de la candidate d’exemple Nina Demo, avec le menu Étapes ouvert. La commande à utiliser est entourée en orange. </figcaption> </figure> Certaines étapes demandent un peu plus : * **Vérifiez votre adresse e-mail** : saisissez votre e-mail et sélectionnez **Envoyer le code**. Saisissez le code reçu dans **Code de vérification à six chiffres**, puis sélectionnez **Continuer**. **Renvoyer le code** en envoie un nouveau après une courte attente, et **Modifier l’e-mail** permet de corriger l’adresse. Si vous avez déjà un compte ou une candidature avec cette adresse, un message vous propose un bouton pour y accéder, comme **Accéder à mon compte** ou **Terminer ma candidature**. Une fois vérifiée, l’étape affiche votre **E-mail vérifié** : sélectionnez **Modifier l’e-mail** pour le changer avec un nouveau code, puis **Enregistrer l’e-mail**. * **Parrains** : demandez leurs codes de parrainage à des membres du club et saisissez-les dans **Code de parrainage 1**, **Code de parrainage 2**, etc. * Plan : choisissez le plan qui vous intéresse. Chaque plan affiche son prix. * Adresse, téléphone, identité et visite : certains clubs les demandent dans la candidature plutôt qu’après l’acceptation. Elles fonctionnent comme décrit dans [Terminer les étapes après l’acceptation](/fr/guide/members/after-your-application#2-terminer-les-%C3%A9tapes-apr%C3%A8s-lacceptation). Une fois votre identité vérifiée, votre nom ne peut plus être modifié ici, pas plus que les étapes d’identité et de visite une fois terminées. ## 2. Signer un document [#2-signer-un-document] Quand le club vous demande de signer un document, comme son règlement, l’étape affiche le nom du document et un lien **Télécharger le document** pour le lire d’abord. 1. Sélectionnez **Continuer vers Yousign**. Yousign est le service de signature en ligne utilisé par le club. 2. Signez dans la fenêtre Yousign. Selon le club, elle s’ouvre dans la page sous **Signer avec Yousign** ou sur le site de Yousign. 3. Une fois la signature faite, vous revenez automatiquement à votre candidature. Si la fenêtre de signature reste vide, sélectionnez **Ouvrir Yousign dans un nouvel onglet**. Après avoir signé, revenez et sélectionnez **Vérifier la signature**. Pendant que Yousign la confirme, la page affiche **Confirmation de votre signature…**. Si la page de signature indique **Lien de signature indisponible**, sélectionnez **Revenir à la candidature** et repassez par Yousign. > Dès que vous commencez la signature, vos réponses précédentes ne peuvent plus être modifiées. > Vérifiez-les avant de sélectionner **Continuer vers Yousign**. ## 3. Relire et envoyer [#3-relire-et-envoyer] À la dernière étape, sélectionnez **Examiner la candidature**. La page **Vérifiez votre candidature** liste toutes vos réponses. Sélectionnez **Modifier** à côté d’une réponse pour la changer. Quand tout est correct, sélectionnez **Envoyer la candidature**. Votre candidature affiche maintenant **En attente de décision**. Vous n’avez plus rien à faire jusqu’à la réponse du club. <figure> <img src="/images/guide/fr/members/application-submitted.png" alt="Candidature de Lucas Demo à Ukab Clubs avec le statut En attente de décision, le titre Candidature envoyée et un texte indiquant que le club va étudier la candidature et qu’aucune action n’est nécessaire." width="1152" height="304" /> <figcaption> La candidature envoyée du candidat d’exemple Lucas Demo. </figcaption> </figure> Pour suivre votre candidature plus tard, utilisez **Retrouver ma candidature** comme décrit dans [Trouver un club et candidater](/fr/guide/members/find-a-club-and-apply#4-retrouver-votre-candidature). Le travail du club sur cette candidature est décrit dans [Étudier les candidatures](/fr/guide/staff/applications). Une fois la décision du club prise, consultez [Après la décision du club](/fr/guide/members/after-your-application). # Billets, séjours et personnes de votre réservation Ajouter une réservation à votre calendrier, télécharger son billet, suivre votre séjour et inviter les personnes qui vous accompagnent. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/tickets-stays-and-people La page d’une réservation confirmée vous donne ce qu’il faut le jour venu : un fichier calendrier, un billet quand votre club en délivre, et l’étape de votre séjour. C’est aussi là que vous invitez les personnes qui vous accompagnent, et que vous répondez aux réservations auxquelles d’autres membres vous invitent. Pour ouvrir une réservation, consultez [Ouvrir une réservation](/fr/guide/members/reservations-and-coaching#1-ouvrir-une-r%C3%A9servation). ## 1. Ajouter une réservation confirmée à votre calendrier [#1-ajouter-une-réservation-confirmée-à-votre-calendrier] Quand une réservation est **Confirmée**, sélectionnez **Ajouter à mon calendrier** en haut de sa page. Un fichier calendrier se télécharge : ouvrez-le pour ajouter la réservation au calendrier de votre téléphone ou de votre ordinateur. La page confirme **Fichier calendrier téléchargé**. <figure> <img src="/images/guide/fr/members/reservation-confirmed.png" alt="Réservation confirmée de Place à la piscine sur Transat #4 avec un bouton Ajouter à mon calendrier entouré en orange, des boutons pour modifier et annuler la réservation, une carte de détails avec les heures de début et de fin, un prix de 30 Stars et 2 personnes, et la carte Personnes sur cette réservation en dessous." width="1280" height="800" /> <figcaption> La réservation payée d’Alex Morgan à la piscine pour deux personnes, dans trois jours. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 2. Télécharger votre billet [#2-télécharger-votre-billet] Si votre club délivre des billets, la page comporte aussi une carte **Billet de réservation**. Sélectionnez **Télécharger le billet PDF** pour l’obtenir. Le QR code reste le même quand la réservation change : téléchargez de nouveau le PDF pour avoir les dates à jour. Les billets ne sont disponibles que pour les réservations confirmées. <figure> <img src="/images/guide/fr/members/reservation-ticket.png" alt="Carte Billet de réservation expliquant que le QR code reste le même quand la réservation change, avec un bouton Télécharger le billet PDF entouré en orange." width="1056" height="144" /> <figcaption> La carte du billet de la réservation confirmée d’Alex Morgan pour Concert — matinée, pour deux personnes. </figcaption> </figure> ## 3. Suivre votre séjour [#3-suivre-votre-séjour] De l’heure d’arrivée à l’heure de départ, une réservation confirmée affiche l’étape de votre séjour à la place de son statut : **Arrivée**, **En cours**, **Départ**, puis **Parti** une fois que le club a enregistré votre départ. Les extras que vous avez choisis, comme des boissons, sont listés sous **Produits facturés** en haut de la page. Quand le club conserve une caution, c’est-à-dire une somme mise de côté en garantie, la carte de détails indique son montant et son **Statut de la caution**. Les autres lignes de montants sont expliquées dans [Lire les montants](/fr/guide/members/reservations-and-coaching#2-lire-les-montants). <figure> <img src="/images/guide/fr/members/reservation-stay.png" alt="Séjour Suite d’hôtel dans la Garden suite avec la mention En cours, des Produits facturés listant Welcome champagne × 1 pour 65 Stars, un bouton Ajouter à mon calendrier, et une carte de détails avec les dates de début et de fin, le Prix de la réservation de 887 Stars, 2 personnes, un Total avec caution de 1 387 Stars, une Caution de 500 Stars, le Statut de la caution Retenue jusqu’au remboursement et la ligne Annulation Prix non remboursable : le délai d’annulation est dépassé." width="1280" height="800" /> <figcaption> Le séjour de Jamie Chen dans la Garden suite, commencé hier, avec sa caution payée. </figcaption> </figure> ## 4. Inviter les personnes qui vous accompagnent [#4-inviter-les-personnes-qui-vous-accompagnent] Quand une réservation est pour plusieurs personnes, sa page comporte une carte **Personnes sur cette réservation**. Elle compte les places occupées, par exemple **2 / 2 occupants**, et liste les personnes invitées avec leur réponse : **Invité**, **Acceptée** ou **Refusé**. **Compte associé** signifie que la personne a déjà un compte au club. 1. Saisissez l’**E-mail** de la personne qui vous accompagne. 2. Si votre club demande aux invités d’avoir un compte, choisissez la **Durée d’accès** de leur compte temporaire : **12 heures**, **24 heures** ou **48 heures**. 3. Sélectionnez **Envoyer l’invitation**. La personne reçoit un e-mail pour accepter ou refuser. Sélectionnez **Supprimer** pour retirer quelqu’un de la réservation. Quand toutes les places sont prises, le formulaire l’indique : retirez une personne pour en inviter une autre. Une invitation expirée sans réponse garde sa place jusqu’à ce que vous la retiriez. <figure> <img src="/images/guide/fr/members/reservation-people.png" alt="Carte Personnes sur cette réservation affichant 2 / 2 occupants, Jamie Chen invité avec la mention Compte associé et un bouton Supprimer entouré en orange, et un champ E-mail vide avec la note Toutes les places sont attribuées : retirez une personne pour en inviter une autre." width="1056" height="356" /> <figcaption> Jamie Chen, invité à la réservation d’exemple à la piscine, n’a pas encore répondu. Toutes les places sont prises : il faut en libérer une avant une nouvelle invitation. </figcaption> </figure> ## 5. Les réservations auxquelles vous êtes invité [#5-les-réservations-auxquelles-vous-êtes-invité] Un autre membre peut vous inviter à rejoindre sa réservation. Ses invitations apparaissent dans **Réservations auxquelles vous êtes invité** en bas de l’onglet **Réservations**, avec le lieu, le statut de la réservation, **Invité par** suivi du nom de l’hôte, la date et votre réponse. <figure> <img src="/images/guide/fr/members/invited-reservations.png" alt="Carte Réservations auxquelles vous êtes invité avec Transat #4, Confirmée, Invité par Alex Morgan avec la date et l’heure de la réservation, une mention Invité, et des boutons Accepter l’invitation et Refuser, Accepter l’invitation étant entouré en orange." width="1056" height="196" /> <figcaption> L’invitation de Jamie Chen à la réservation d’Alex Morgan à la piscine. </figcaption> </figure> Sélectionnez **Accepter l’invitation** ou **Refuser**. Votre réponse affiche ensuite **Acceptée** ou **Refusé**, et **Invitation indisponible** signifie que l’invitation a expiré. Une fois que vous avez accepté une réservation confirmée qui comporte un billet, sélectionnez **Télécharger le billet** pour l’obtenir. Pour répondre depuis l’e-mail d’invitation, consultez [Accepter une invitation reçue par e-mail](/fr/guide/members/invitations-from-email). # Votre espace membre Vous repérer dans votre espace membre, signaler votre arrivée au club et mettre à jour votre profil. Source: https://docs.champagne.dev-ukab.fr/fr/guide/members/your-account Votre [espace membre](/fr/guide#member-app) rassemble tout ce qui concerne votre adhésion : vos réservations, votre facturation, vos invitations et vos échanges avec le club. Connectez-vous à l’espace membre et sélectionnez **Mon compte** pour l’ouvrir. Cette page présente ce que vous voyez en premier et où mène chaque section. ## 1. Lire l’en-tête [#1-lire-len-tête] Le haut de la page affiche votre nom, le statut de votre compte et votre club. **Actif** signifie que vous pouvez profiter du club et réserver. Les autres statuts sont expliqués dans [Quand votre compte est limité](/fr/guide/members/account-status). Trois boutons se trouvent à droite : * l’icône **Boîte de réception** ouvre vos messages et vos notifications. Le nombre affiché dessus compte les conversations et les notifications non lues, * **Aide** ouvre la page de ce guide qui explique l’écran où vous êtes, * **Se déconnecter** ferme votre session sur cet appareil. Sous l’en-tête, quatre onglets mènent aux sections de votre espace membre : **Aperçu**, **Réservations**, **Facturation** et **Invitations**. <figure> <img src="/images/guide/fr/members/account-overview.png" alt="Onglet Aperçu de l’espace membre d’Alex Morgan avec sa dernière entrée et un bouton Je suis dans le bâtiment entouré en orange, une carte Adhésion pour Adhésion 51 IÉNA (2026) - Abonnement Membre 2026 valable jusqu’au 31 décembre 2026, une carte Prochaine réservation pour une Suite d’hôtel marquée Réservée — paiement dû, une carte Facturation indiquant 3 réservations à payer avec 860 Stars à payer, et les cartes Mon profil et Mon club." width="1280" height="800" /> <figcaption> L’onglet Aperçu du membre d’exemple Alex Morgan chez Ukab Clubs. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 2. Signaler votre arrivée [#2-signaler-votre-arrivée] La première carte de l’onglet **Aperçu** enregistre vos visites. Quand vous arrivez au club, sélectionnez **Je suis dans le bâtiment**. La carte affiche alors l’heure de votre dernière entrée, par exemple **Dernière entrée aujourd’hui à** suivi de l’heure, ou **Dernière entrée le** suivi de la date d’une visite précédente. Avant votre première visite, elle indique **Aucune entrée enregistrée**. ## 3. Voir votre compte en un coup d’œil [#3-voir-votre-compte-en-un-coup-dœil] Trois cartes résument votre compte. Sélectionnez une carte pour ouvrir la page correspondante. | Carte | Ce qu’elle affiche | Ouvre | | ------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------- | | **Adhésion** | Votre plan ou **Adhésion valide**, avec sa date de fin, ou **Aucune adhésion pour le moment** | Votre page d’adhésion | | **Prochaine réservation** | Votre prochaine réservation avec sa date et son statut, ou **Aucune réservation** | La page de détail de cette réservation | | **Facturation** | Les factures dues, sinon les réservations à payer, ou **Tout est réglé** | Vos factures ou vos réservations | La carte **Facturation** compte d’abord les factures dues. Quand vous ne devez aucune facture mais que des réservations sont **Réservée — paiement dû**, elle affiche **1 réservation à payer** ou le nombre de réservations, avec leur total en dessous, par exemple **… Stars à payer**. La carte ouvre alors vos réservations pour que vous les payiez. Plus bas, **Mon club** affiche la description et l’adresse de votre club. La carte **Je suis dans le bâtiment** et la carte **Prochaine réservation** n’apparaissent que tant que vous pouvez réserver : elles sont masquées quand votre compte est suspendu, banni ou désactivé. Les comptes invités voient **Prochaine réservation** sans les cartes **Adhésion** et **Facturation**. ## 4. Mettre à jour votre profil [#4-mettre-à-jour-votre-profil] La carte **Mon profil** affiche les informations que votre club a enregistrées. Pour les modifier : 1. Sélectionnez **Modifier**. 2. Mettez à jour votre prénom et votre nom, votre **Téléphone**, votre **Date de naissance**, votre **Genre** ou votre adresse. 3. Sélectionnez **Enregistrer les modifications**, ou **Annuler** pour tout laisser comme avant. <figure> <img src="/images/guide/fr/members/profile-editor.png" alt="Carte Mon profil en mode modification avec le prénom Alex, le nom Morgan, un numéro de téléphone en cours de saisie, les champs date de naissance, genre et adresse, et les boutons Enregistrer les modifications et Annuler, Enregistrer les modifications entouré en orange." width="696" height="656" /> <figcaption> Le profil d’Alex Morgan, avec un numéro de téléphone d’exemple en cours d’ajout. </figcaption> </figure> Dès que vous commencez une adresse, ses champs principaux deviennent obligatoires. Pour supprimer une adresse, videz tous ses champs. ## 5. Trouver les autres sections [#5-trouver-les-autres-sections] Certains onglets regroupent plusieurs pages. Sélectionnez le nom d’une page en haut de l’onglet pour passer de l’une à l’autre. | Onglet ou bouton | Pages | Guide | | ---------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **Réservations** | Vos réservations et le planning du club | [Faire une réservation](/fr/guide/members/book-a-reservation), [Vos réservations](/fr/guide/members/reservations-and-coaching) | | **Facturation** | **Adhésion**, **Crédits**, **Factures** | [Votre adhésion](/fr/guide/members/membership-and-credits), [Vos crédits](/fr/guide/members/credits), [Facturation](/fr/guide/members/billing) | | **Invitations** | **Invités du club**, **Parrainage** | [Invités et parrainage](/fr/guide/members/guests-and-sponsorship) | | Icône **Boîte de réception** | **Messages**, **Notifications** | [Messages](/fr/guide/members/messages), [Notifications](/fr/guide/members/notifications) | > Les onglets **Réservations** et **Invitations** apparaissent tant que votre compte est actif. Les > comptes invités ne voient que les pages utiles à leur visite. Contactez votre club s’il vous > manque une section. Quand votre club limite votre compte, ou qu’une page demande une adhésion valide, consultez [Quand votre compte est limité](/fr/guide/members/account-status). # Votre compte Modifiez votre nom, votre photo, votre biographie et votre mot de passe, consultez vos diplômes, ou déconnectez-vous. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/account La page **Mon compte** affiche le profil d’équipe que vous utilisez dans le [back-office](/fr/guide#back-office). De là, vous pouvez modifier votre nom, votre photo et votre biographie, changer votre mot de passe et vous déconnecter. ## 1. Ouvrir votre compte [#1-ouvrir-votre-compte] Dans le menu de gauche, sélectionnez **Mon compte**. La page affiche votre nom, votre adresse e-mail, votre **Biographie** et vos **Diplômes et carte professionnelle** quand vous en avez. <figure> <img src="/images/guide/fr/staff/account.png" alt="Page Mon compte du compte Sport coach avec son adresse e-mail, une biographie, trois éléments sous Diplômes et carte professionnelle (un certificat de premiers secours, un brevet de surveillant de baignade marqué Expiré et une carte professionnelle), et les boutons Modifier le profil, Voir la fiche membre, Modifier le mot de passe et Se déconnecter." width="1280" height="800" /> <figcaption> Le compte du coach d’exemple Sport coach, avec deux diplômes et une carte professionnelle. </figcaption> </figure> ## 2. Modifier votre profil [#2-modifier-votre-profil] Les membres voient votre nom, votre photo et votre biographie quand vous échangez avec eux. Sélectionnez **Modifier le profil** pour les changer. 1. Sélectionnez le cercle de la photo pour ajouter une photo, ou pour remplacer ou retirer la photo actuelle. Vous pouvez ensuite ajuster le cadrage et sélectionner **Appliquer le cadrage**. 2. Modifiez votre **Nom** si besoin. 3. Écrivez quelques mots sur vous dans **Biographie**. 4. Ouvrez l’onglet **Aperçu** pour voir votre profil tel que les membres le voient. 5. Sélectionnez **Enregistrer les modifications**. <figure> <img src="/images/guide/fr/staff/edit-profile.png" alt="Fenêtre Modifier le profil avec les onglets Modifier et Aperçu, un cercle pour la photo, le nom Sport coach et une biographie d’exemple, avec les boutons Annuler et Enregistrer les modifications." width="1280" height="800" /> <figcaption> Ajout d’une biographie d’exemple au profil Sport coach. La commande à utiliser est entourée en orange. </figcaption> </figure> Pour consulter votre profil plus tard sans le modifier, sélectionnez **Voir la fiche membre**. ### Diplômes et carte professionnelle [#diplômes-et-carte-professionnelle] Quand votre administrateur enregistre un diplôme ou une carte professionnelle dans **Équipe**, il apparaît sous **Diplômes et carte professionnelle** sur votre compte et dans le profil que voient les membres. Chacun affiche son type (**Diplôme** ou **Carte professionnelle**), son intitulé, l’organisme qui l’a délivré, et les dates **Délivré le** et **Expire le** quand elles sont connues. Un document expiré affiche **Expiré**. Les membres ne voient ni le numéro du document ni le fichier, et les autres types de documents n’apparaissent pas. Pour ajouter ou mettre à jour un diplôme ou une carte professionnelle, adressez-vous à votre administrateur. Voir [Gérer votre équipe](/fr/guide/admins/manage-your-team). ## 3. Changer votre mot de passe [#3-changer-votre-mot-de-passe] Sélectionnez **Modifier le mot de passe**, saisissez votre **Mot de passe actuel**, puis votre **Nouveau mot de passe**, et répétez-le dans **Confirmer le mot de passe**. Un mot de passe doit compter au moins 12 caractères, dont une minuscule, une majuscule, un chiffre et un symbole. Sélectionnez **Enregistrer les modifications**. > Après le changement de votre mot de passe, Champagne vous déconnecte. Reconnectez-vous avec votre > nouveau mot de passe. Si vous avez oublié votre mot de passe actuel, déconnectez-vous et sélectionnez **Mot de passe oublié ?** sur la page de connexion. ## 4. Changer de langue ou vous déconnecter [#4-changer-de-langue-ou-vous-déconnecter] Les boutons de langue et de mode sombre se trouvent en haut à droite de chaque page. Voir [Se connecter et se repérer](/fr/guide/staff/getting-started). Pour quitter le back-office, sélectionnez **Se déconnecter** en bas de la carte de votre compte. # Étudier les candidatures Lisez une candidature, puis acceptez-la ou refusez-la. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/applications Quand une personne veut rejoindre votre club, elle remplit votre formulaire de [candidature](/fr/guide#application) en ligne. La section **Candidatures** liste ces candidatures pour que votre équipe puisse lire chacune d’elles, puis l’accepter ou la refuser. Pour modifier les questions du formulaire lui-même, voir [Formulaires de candidature](/fr/guide/admins/onboarding-forms). ## 1. Voir les candidatures [#1-voir-les-candidatures] Dans le menu de gauche, sélectionnez **Candidatures**. La liste affiche chaque **Candidat**, la date d’envoi de la candidature (**Envoyée**) et son **Statut**. <figure> <img src="/images/guide/fr/staff/applications.png" alt="Section Candidatures avec un champ de recherche, les boutons Gérer les parcours et Filtres (1), et une liste de candidats avec leur date d’envoi et un statut Envoyée ou Acceptée." width="1280" height="800" /> <figcaption> Des candidatures d’exemple à Ukab Clubs, en attente ou déjà acceptées. </figcaption> </figure> | Statut | Sens | | --------------------- | --------------------------------------------------------------------------------------------------------------- | | **Brouillon** | Le candidat a commencé le formulaire et ne l’a pas encore envoyé. Sa date affiche **Non envoyée**. | | **Envoyée** | Le candidat a envoyé le formulaire. La candidature attend une décision. | | **En cours d’examen** | Elle attend aussi une décision. La liste l’affiche comme **Envoyée** et les filtres la comptent avec celles-ci. | | **Acceptée** | Votre équipe a accepté la candidature. | | **Refusée** | Votre équipe a refusé la candidature. | Un brouillon dont le candidat n’a pas encore donné de nom s’intitule **Candidature en cours**. Une candidature acceptée peut aussi afficher des **Étapes**, comme **Étapes 0/1**. Ce sont les étapes que le nouveau membre doit encore réaliser après l’acceptation, par exemple payer son [plan](/fr/guide#plan). Par défaut, la liste affiche les candidatures **Envoyée** et **Acceptée**. Sélectionnez **Filtres** pour choisir d’autres statuts ou une période d’envoi, puis sélectionnez **Appliquer les filtres**. Saisissez un nom ou une adresse e-mail dans **Rechercher par nom ou e-mail…** pour trouver un candidat. Quand rien ne correspond, sélectionnez **Effacer la recherche et les filtres**. ## 2. Lire une candidature [#2-lire-une-candidature] Sélectionnez un candidat dans la liste. La page affiche son nom et son statut, la **Version du parcours** (la version du formulaire qu’il a remplie) et la date d’envoi, puis : * **Parrains**, quand votre formulaire demande des codes de parrainage : les membres dont le candidat a utilisé le code, avec un lien vers leur profil, ou **Aucun code de parrainage n’a encore été utilisé.** * **Motif du refus**, pour une candidature refusée avec un motif. * **Informations envoyées** : chaque question de votre formulaire avec la réponse du candidat. Une question laissée vide affiche **Sans réponse** quand elle était facultative et que la candidature a été envoyée, et **Non terminé** sinon. La pastille à côté du titre compte les questions qui ont une réponse. Les questions intégrées portent le nom qu’elles ont dans l’éditeur de parcours, comme **Nom du candidat**, **Adresse du candidat**, **Choix du plan**, **Parrainage** et **E-mail vérifié**. Quand votre formulaire les demande avant acceptation, le résultat du contrôle d’identité et la visite réservée apparaissent aussi ici, et les actions d’identité ci-dessous fonctionnent de la même façon. * **Après acceptation** : les étapes que le candidat réalise une fois accepté, si votre formulaire en a. Chaque étape affiche **En attente d’acceptation** avant la décision, **Non terminé** après, puis la réponse et sa date, ou **Paiement confirmé** pour un paiement. Une étape d’identité affiche aussi ce que Stripe a contrôlé : **Document**, **Selfie**, **Date de naissance** avec le contrôle d’âge, et **Nom**. Tant que l’étape est celle en cours pour le candidat, sélectionnez **Autoriser une nouvelle tentative** pour qu’il réessaie, par exemple après **Sous l’âge minimum** ou **Le nom ne correspond pas à la candidature**, ou **Marquer comme vérifiée** pour la terminer vous-même. Champagne demande d’abord **Marquer l’identité comme vérifiée ?**, car Stripe n’a pas confirmé tous les contrôles. * **Historique des statuts** : quand la candidature a été commencée, envoyée et décidée. Sélectionnez un changement pour voir qui l’a fait, avec le motif s’il y en a un : un membre de l’équipe, **Système** pour les changements automatiques, **Candidat · identité non authentifiée** pour le candidat, ou **Équipe · identité indisponible** quand aucun nom n’est enregistré pour le membre de l’équipe à l’origine du changement. * **Historique des e-mails** : chaque e-mail envoyé au candidat, comme l’e-mail d’**Acceptation** ou de **Refus** ou un **Rappel de finalisation**, avec son état : **Envoi en cours**, **Accepté par le fournisseur**, **Échec** ou **Incertain**. **Accepté par le fournisseur** signifie que l’e-mail a quitté Champagne ; cela ne confirme pas qu’il est arrivé dans la boîte de réception. * **Historique des SMS**, quand votre formulaire vérifie le téléphone du candidat, avant ou après acceptation : chaque code envoyé par SMS, marqué **SMS envoyé**, **SMS reçu** ou **SMS non reçu**. <figure> <img src="/images/guide/fr/staff/application.png" alt="Candidature de Lucas Astel au statut Envoyée, avec les boutons Accepter et Refuser tous deux entourés en orange, la carte Informations envoyées listant trois réponses et le début de l’Historique des statuts." width="1280" height="800" /> <figcaption> La candidature du candidat d’exemple Lucas Astel. Les commandes à utiliser sont entourées en orange. </figcaption> </figure> <figure> <img src="/images/guide/fr/staff/application-sponsors.png" alt="Candidature de Lucas Demo, Envoyée, avec les boutons Accepter et Refuser, une carte Parrains listant Alex Morgan et son adresse e-mail, et la carte Informations envoyées avec la réponse Alex Morgan sous Parrainage." width="1280" height="800" /> <figcaption> Le candidat d’exemple Lucas Demo, qui a utilisé le code de parrainage d’Alex Morgan. </figcaption> </figure> ## 3. Accepter ou refuser [#3-accepter-ou-refuser] Sélectionnez **Accepter** ou **Refuser**. Une fenêtre de confirmation s’ouvre. Dès qu’une candidature est liée à une personne dans **Personnes**, les deux boutons disparaissent et **Ouvrir la fiche** les remplace en haut de la page. Une candidature pas encore liée n’affiche que la décision qu’elle n’a pas : **Refuser** sur une candidature acceptée, **Accepter** sur une candidature refusée. * **Accepter** : sélectionnez **Confirmer l’acceptation**. Champagne crée le profil de la personne et lui envoie un e-mail pour poursuivre son inscription. Une visite réservée pendant la candidature est rattachée à son profil. Un bouton **Ouvrir la fiche** vous mène ensuite à son profil dans **Personnes**. * **Refuser** : vous pouvez saisir un **Motif (facultatif)**, puis sélectionner **Confirmer le refus**. Le candidat reçoit un e-mail qui contient votre motif. Une visite réservée pendant la candidature est annulée. <figure> <img src="/images/guide/fr/staff/reject-application.png" alt="Fenêtre Refuser la candidature ? avec un motif d’exemple, Our membership is full for this season, et les boutons Annuler et Confirmer le refus." width="1280" height="800" /> <figcaption> Le refus de la candidature d’exemple de Lucas Astel avec un court motif. </figcaption> </figure> > Un candidat refusé qui essaie de candidater à nouveau est invité à contacter d’abord votre club. > Si vous changez d’avis, ouvrez la candidature et sélectionnez **Accepter**. # Périodes bloquées, séances et listes d’attente Fermez temporairement une ressource, retrouvez les séances d’un cours collectif et consultez la liste d’attente. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/blocked-periods-and-waitlists Les boutons en haut à droite de l’agenda dans **Réservations** permettent de fermer temporairement une ressource, de lister les séances d’un cours collectif et de voir qui attend une place. Les boutons affichés dépendent de la catégorie et de votre [rôle](/fr/guide#role). Pour lire l’agenda, voir [Gérer les réservations](/fr/guide/staff/reservations). ## 1. Bloquer une période [#1-bloquer-une-période] Une période bloquée ferme temporairement une ressource, par exemple pendant des travaux d’entretien. Personne ne peut la réserver. **Périodes bloquées** ouvre **Gérer les périodes bloquées**. Dans **S’applique à**, choisissez **Toutes les ressources** ou l’une d’elles. Saisissez le début et la fin, le **Motif (facultatif)**, puis sélectionnez **Bloquer une période**. La période apparaît en rouge dans l’agenda. Vous pouvez aussi ouvrir l’onglet **Période bloquée** en haut du formulaire **Ajouter une réservation**. Remplissez les dates et un motif facultatif, puis sélectionnez **Bloquer une période**. Cet onglet n’apparaît que si votre rôle vous permet de modifier la catégorie. ## 2. Rouvrir une période bloquée [#2-rouvrir-une-période-bloquée] Pour rouvrir une période bloquée, sélectionnez son bloc rouge **Bloqué** dans l’agenda. La fenêtre affiche les dates et le motif. Sélectionnez **Supprimer le bloc** pour que la période soit de nouveau réservable. <figure> <img src="/images/guide/fr/staff/remove-blocked-period.png" alt="Fenêtre de suppression d’une période bloquée pour un jour à venir de 09:00 à 11:00 avec le motif Préparation et entretien de l'espace, et les boutons Annuler et Supprimer le bloc." width="384" height="276" /> <figcaption> Une période bloquée de la salle de réunion d’exemple Salle de réunion. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 3. Retrouver les séances d’un cours collectif [#3-retrouver-les-séances-dun-cours-collectif] **Gérer les séances** remplace **Périodes bloquées** dans les catégories de cours collectifs. Ce bouton liste les séances que les membres peuvent réserver (**Datés** et **Récurrents**), et **Ajouter une séance** en crée une. Une icône d’avertissement à côté du bouton signale que la catégorie n’a encore aucune séance. En général, c’est votre administrateur qui crée les séances. Voir [Séances, listes d’invités et périodes bloquées](/fr/guide/admins/sessions-and-blocked-periods). ## 4. Consulter la liste d’attente [#4-consulter-la-liste-dattente] **Liste d’attente** apparaît dans les catégories où les membres choisissent un horaire et un nombre de personnes. Elle liste qui attend une place dans les 30 prochains jours, avec la date et l’heure souhaitées, le nombre de personnes et leur rang dans la file. Quand personne n’attend, elle affiche **Personne n’attend dans les 30 prochains jours.** <figure> <img src="/images/guide/fr/staff/reservations-waitlist.png" alt="Fenêtre Liste d’attente avec Gabriel Morel pour 2 personnes au rang #1 et Arthur Perrin pour 4 personnes au rang #2, tous deux pour la même soirée à 20:00." width="448" height="250" /> <figcaption> Deux groupes en attente d’une table au restaurant d’exemple Le Rooftop. </figcaption> </figure> Quand une place se libère, par exemple après une annulation, Champagne la réserve en moins d’une minute pour les personnes en attente, dans l’ordre de la file, avec un éventuel paiement dû. La place ne reste pas libre pour quelqu’un à l’accueil. Pour les cours collectifs, la liste d’attente de chaque séance apparaît dans son panneau, dans **Agenda du club**. Voir [Utiliser l’agenda du club](/fr/guide/staff/planning#2-suivre-une-s%C3%A9ance-et-marquer-la-pr%C3%A9sence). # Répondre aux messages Lisez les messages des membres et répondez-y depuis la boîte de réception Conversations, et lancez une nouvelle conversation. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/chats La section **Conversations** est la boîte de réception de votre club. Les membres écrivent au club depuis leur compte membre, et votre équipe leur répond ici. Cette page montre comment repérer les nouveaux messages, répondre, lancer une conversation et ranger la boîte de réception. ## 1. Ouvrir la boîte de réception [#1-ouvrir-la-boîte-de-réception] Dans le menu de gauche, sélectionnez **Conversations**. Chaque ligne est une conversation avec un membre : son objet, le nom du membre, le dernier message et son heure d’arrivée. Les messages du jour affichent l’heure, les plus anciens affichent la date. <figure> <img src="/images/guide/fr/staff/chats.png" alt="Boîte de réception Conversations avec deux conversations avec Alex Morgan : Locker for the season, en gras avec un point car non lue, et Parking during the tournament." width="1280" height="800" /> <figcaption> La boîte de réception du club d’exemple Ukab Clubs, avec une conversation non lue d’Alex Morgan. </figcaption> </figure> Une conversation qui contient un nouveau message du membre se repère à : * un point sur les initiales du membre, * l’objet en gras et le dernier message en texte plus foncé, * l’heure en couleur. Un point sur **Conversations** dans le menu de gauche vous signale qu’au moins une conversation est non lue, où que vous soyez dans le [back-office](/fr/guide#back-office). ## 2. Répondre à un membre [#2-répondre-à-un-membre] Sélectionnez une conversation pour l’ouvrir. Les messages du membre sont à gauche, les réponses de votre équipe à droite, chacun avec le nom de l’expéditeur et la date. Ouvrir la conversation la marque comme lue. 1. Saisissez votre réponse dans la zone **Écrivez un message…** en bas. 2. Sélectionnez **Envoyer**, ou appuyez sur <kbd>Entrée</kbd>. Appuyez sur <kbd>Maj</kbd> + <kbd>Entrée</kbd> pour aller à la ligne sans envoyer. <figure> <img src="/images/guide/fr/staff/chat-conversation.png" alt="Conversation Parking during the tournament avec Alex Morgan, montrant un message de Club team et une réponse en cours de saisie à côté du bouton Envoyer." width="1280" height="800" /> <figcaption> Une réponse en cours d’écriture dans la conversation d’exemple Parking during the tournament. La commande à utiliser est entourée en orange. </figcaption> </figure> Vos réponses sont signées du nom de votre compte d’équipe. Les messages envoyés par un compte sans nom sont signés **Club team**. Sélectionnez **Ouvrir la fiche**, affiché si vous pouvez voir les fiches des membres, pour ouvrir la page du membre dans **Personnes**, et **Conversations** pour revenir à la boîte de réception. ## 3. Lancer une nouvelle conversation [#3-lancer-une-nouvelle-conversation] Pour écrire en premier à quelqu’un, sélectionnez **Nouvelle conversation** dans la boîte de réception. 1. Dans **Personne**, saisissez quelques lettres de son nom dans **Rechercher une personne** et choisissez-la dans la liste, qui contient les mêmes personnes que la section **Personnes**. 2. Saisissez un **Objet**, comme le sujet de votre message. 3. Écrivez votre **Message**. 4. Sélectionnez **Envoyer**. La nouvelle conversation s’ouvre et apparaît dans la boîte de réception. <figure> <img src="/images/guide/fr/staff/new-chat.png" alt="Fenêtre Nouvelle conversation avec Alex Morgan choisi comme Personne, l’objet Court lights on Friday et un court message, avec les boutons Annuler et Envoyer." width="1280" height="800" /> <figcaption> Une nouvelle conversation d’exemple pour Alex Morgan, prête à être envoyée. </figcaption> </figure> ## 4. Archiver les conversations terminées [#4-archiver-les-conversations-terminées] Sélectionnez l’icône de boîte en bout de ligne pour **Archiver** une conversation dont vous n’avez plus besoin dans la boîte de réception. Basculez entre **Actif** et **Archivé** en haut de la liste pour voir chaque groupe, et sélectionnez la même icône dans **Archivé** pour **Restaurer** une conversation. Une boîte de réception vide affiche **Aucune conversation active pour le moment.** ou **Aucune conversation archivée.** Si elle affiche **Conversations indisponibles**, la boîte de réception n’a pas pu se charger : rechargez la page, et prévenez votre administrateur si le message persiste. ## 5. Messages liés à une réservation [#5-messages-liés-à-une-réservation] Les membres de l’équipe qui ont le [rôle](/fr/guide#role) **Équipe**, comme les coachs, peuvent aussi recevoir des messages sur les réservations. Pour eux, la boîte de réception a deux onglets : **Clients** pour les conversations ci-dessus et **Réservations** pour les messages liés à une réservation. Chaque ligne de réservation affiche le nom du client, l’activité, la ressource, la date de la séance, ou **Horaire à convenir** quand aucun horaire n’est encore fixé, et le statut de la réservation : | Statut | Ce qu’il signifie | | -------------------------- | -------------------------------------------------- | | **En attente** | La réservation attend la réponse du club. | | **Réservée · paiement dû** | L’horaire est fixé et le client doit encore payer. | | **Confirmée** | La réservation est confirmée. | | **Terminé** | La réservation est marquée comme terminée. | | **Annulée** | La réservation a été annulée. | <figure> <img src="/images/guide/fr/staff/chats-reservations.png" alt="Onglet Réservations de Conversations pour un coach, entouré en orange, listant trois conversations de cours particuliers avec Alex Morgan, Sam Rivera et Jamie Chen, avec la date de leur séance ou Horaire à convenir, et le statut de leur réservation." width="1280" height="800" /> <figcaption> Les conversations de réservation du compte de coach d’exemple Sport coach. </figcaption> </figure> Sélectionner une conversation de réservation ouvre la réservation dans **Agenda du club**, où vous lisez les messages et y répondez à côté des détails de la séance. > Les onglets affichés dépendent de vos autorisations. **Clients** nécessite l’autorisation > **Discuter avec les clients**, et **Réservations** nécessite **Gérer les messages de > réservation**. Si vous devez répondre aux conversations des membres ou aux messages de réservation > et que vous ne les voyez pas, adressez-vous à votre administrateur. # Enregistrer l’arrivée et le départ des clients Validez les billets à l’entrée, enregistrez les départs et remboursez les cautions. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/check-in-and-deposits Cette page couvre les moments où un client arrive et repart. Vous pouvez contrôler le billet d’un membre à l’entrée et le marquer présent, suivre un séjour en cours, noter son départ avec le bouton **Enregistrer le départ** et rendre la caution. Une caution est une somme que le client paie en plus du prix et qu’il récupère après le séjour, par exemple pour une suite d’hôtel. ## 1. Contrôler un billet à l’entrée [#1-contrôler-un-billet-à-lentrée] Quand une [catégorie de réservation](/fr/guide#reservation-category) donne aux membres un billet avec QR code, les membres le téléchargent depuis leur réservation. Le billet affiche un QR code et un code à six chiffres. Votre administrateur active cette fonction avec l’option **Billet avec QR code** de la catégorie. Dans **Réservations** ou **Agenda du club**, sélectionnez la catégorie, puis **Billets** au-dessus de l’agenda. Le bouton apparaît pour les catégories avec billets, si votre [rôle](/fr/guide#role) comprend l’autorisation **Valider les présences et les billets**. <figure> <img src="/images/guide/fr/staff/ticket-code.png" alt="Fenêtre Billets avec les choix Scanner et Code, Code sélectionné, le code 510004 saisi dans le champ et un bouton Vérifier le billet." width="384" height="276" /> <figcaption> Saisie à la main du code de billet d’exemple 510004. La commande à utiliser est entourée en orange. </figcaption> </figure> 1. Avec **Scanner**, présentez le QR code du billet devant la caméra de votre appareil. Le billet est lu dès que le code est visible. 2. Avec **Code**, saisissez les six chiffres imprimés sous le QR code, puis sélectionnez **Vérifier le billet**. Utilisez ce choix quand la caméra n’arrive pas à lire le code. 3. Lisez le résultat. * **Billet valide** affiche le nom et la photo du client, la catégorie, la **Date**, la **Ressource**, le nombre de **Participants** et la présence déjà enregistrée. Les personnes invitées par le client sont listées sous **Invités**, marquées **Acceptée** ou **Invité**. * **Billet invalide** signifie que le code est inconnu, ou que la réservation est annulée, pas encore confirmée ou déjà terminée. Les billets d’une catégorie sans billets sont aussi refusés. <figure> <img src="/images/guide/fr/staff/ticket-valid.png" alt="Fenêtre Billets affichant Billet valide pour Alex Morgan dans la catégorie Événement, avec la date, la ressource Event space, 2 participants, une présence non renseignée, et les boutons Vérifier le suivant et Marquer présent." width="384" height="414" /> <figcaption> Le billet du membre d’exemple Alex Morgan pour un concert en matinée. </figcaption> </figure> 4. Sélectionnez **Marquer présent** pour enregistrer l’arrivée du client. Le bouton affiche ensuite **Présent**. 5. Sélectionnez **Scanner le suivant** ou **Vérifier le suivant** pour le client suivant. > Le navigateur doit avoir le droit d’utiliser la caméra. Sinon, un message indique que la caméra > est indisponible et propose de réessayer. Passez à **Code** et saisissez le code à la place. Après > de nombreuses tentatives en peu de temps, le contrôle se met en pause pendant une minute. Vous pouvez aussi noter la présence à la main sur la page de la réservation ou dans le panneau d’une séance. Voir [Gérer les réservations](/fr/guide/staff/reservations) et [Utiliser l’agenda du club](/fr/guide/staff/planning). ## 2. Suivre un séjour et enregistrer le départ [#2-suivre-un-séjour-et-enregistrer-le-départ] Une réservation confirmée indique où en est le séjour dans une étiquette à côté du nom du client. Les horaires viennent de la réservation, et des heures d’arrivée et de départ de la ressource quand votre administrateur les a définies. | Étiquette | Ce qu’elle signifie | | ------------ | -------------------------------------------------------------------------------------- | | **Arrivée** | Le client peut arriver : l’heure d’arrivée est passée. | | **En cours** | La réservation a commencé et n’est pas encore terminée. | | **Départ** | La réservation est terminée et le client peut encore partir jusqu’à l’heure de départ. | | **Parti** | Quelqu’un a enregistré le départ du client. | <figure> <img src="/images/guide/fr/staff/check-out.png" alt="Page de la réservation de Jamie Chen dans la Garden suite pour 2 personnes, marquée En cours, avec une caution de 500 Stars retenue jusqu’au remboursement, les boutons Enregistrer le départ, Modifier et Annuler, et les produits facturés pendant le séjour." width="1280" height="800" /> <figcaption> Le membre d’exemple Jamie Chen pendant un séjour dans la Garden suite. </figcaption> </figure> Pendant le séjour, sélectionnez **Enregistrer le départ** en haut de la page de la réservation quand le client s’en va. **Enregistrer le départ** et **Annuler le départ** nécessitent l’autorisation **Valider les présences et les billets**. L’heure du départ apparaît dans une carte **Parti** à côté des dates. * Sans caution à rendre, la réservation passe à **Terminé**. * Avec une caution payée, elle reste **Confirmée** jusqu’au remboursement de la caution. Si vous avez enregistré le départ du mauvais client, sélectionnez **Annuler le départ**. La réservation revient à **Confirmée**. Une fois la caution remboursée, le départ ne peut plus être annulé. ## 3. Rembourser la caution [#3-rembourser-la-caution] La ligne sous le nom du client montre la caution et où elle en est. | État de la caution | Ce qu’il signifie | | ---------------------------------- | -------------------------------------------------------------- | | **À payer avec la réservation** | Le client n’a pas encore payé la réservation. | | **Retenue jusqu’au remboursement** | Le club garde la caution. | | **Remboursée** | Toute la caution a été rendue au client. | | Un montant remboursé et retenu | Une partie de la caution a été rendue et le reste a été gardé. | **Rembourser la caution** apparaît une fois le client parti, soit après **Enregistrer le départ**, soit une fois l’heure de départ de la réservation passée, si votre rôle comprend l’autorisation **Créer et modifier les réservations**. <figure> <img src="/images/guide/fr/staff/checked-out.png" alt="Page de la réservation de Sam Rivera dans la Terrace suite marquée Parti, avec un bouton Annuler le départ, la ligne de caution de 500 Stars, Retenue jusqu’au remboursement, un bouton Rembourser la caution entouré en orange et une carte Parti à côté des dates de début et de fin." width="1280" height="800" /> <figcaption> Le membre d’exemple Sam Rivera après son départ de la Terrace suite. </figcaption> </figure> 1. Sélectionnez **Rembourser la caution**. 2. Dans **Montant à rembourser**, gardez le montant total ou baissez-le. Le montant retenu apparaît sous le champ. 3. Quand vous gardez une partie de la caution, expliquez pourquoi dans **Message au client**. 4. Sélectionnez **Rembourser en totalité**, ou **Rembourser** suivi du montant. <figure> <img src="/images/guide/fr/staff/refund-deposit.png" alt="Fenêtre Rembourser la caution avec 400 dans Montant à rembourser, 100 Stars retenues, un message au sujet d’une lampe cassée et un bouton Rembourser 400 Stars." width="448" height="392" /> <figcaption> Remboursement de 400 des 500 Stars de la caution d’exemple, 100 étant retenues. </figcaption> </figure> L’argent revient par le moyen de paiement du client : sur sa carte, ou sur son solde de [crédits](/fr/guide#credits). La réservation passe à **Terminé** et le client est prévenu. Votre message apparaît sous la ligne de la caution, et sur la réservation du membre sous **Message du club**. > Les cautions que personne ne rembourse sont rendues en totalité après un délai fixé par votre > administrateur dans **Paramètres**, sous **Cautions**. # Votre premier jour Les cinq tâches à apprendre en premier quand vous rejoignez l’équipe de votre club, dans l’ordre. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/first-day Vous découvrez le [back-office](/fr/guide#back-office) ? Apprenez ces cinq tâches dans cet ordre. Chacune renvoie à la page qui l’explique pas à pas. Ce que vous voyez dépend de votre [rôle](/fr/guide#role) : passez une tâche si sa section n’apparaît pas dans votre menu de gauche. [Qui voit quoi](/fr/guide/admins/who-sees-what) indique ce que chaque autorisation affiche. ### Vous connecter et vous repérer [#vous-connecter-et-vous-repérer]Acceptez votre invitation, connectez-vous et découvrez le menu de gauche. Voir [Se connecter et se repérer](/fr/guide/staff/getting-started). ### Trouver un membre [#trouver-un-membre]Cherchez dans la liste **Personnes** et ouvrez le profil d’un membre. Voir [Gérer les membres](/fr/guide/staff/people). ### Gérer les réservations du jour [#gérer-les-réservations-du-jour]Lisez l’agenda d’une catégorie, ajoutez une réservation, déplacez-la ou annulez-la. Voir [Gérer les réservations](/fr/guide/staff/reservations). ### Accueillir les personnes à l’entrée [#accueillir-les-personnes-à-lentrée]Contrôlez les billets, enregistrez les départs et remboursez les cautions. Voir [Enregistrer l’arrivée et le départ des clients](/fr/guide/staff/check-in-and-deposits). ### Répondre aux messages [#répondre-aux-messages]Lisez les messages des membres et répondez-y dans **Conversations**. Voir [Répondre aux messages](/fr/guide/staff/chats). Une fois ces tâches maîtrisées, le reste de cette partie du guide présente les [candidatures](/fr/guide#application), les profils des membres, les finances, les lettres d’information et l’entretien. # Se connecter et se repérer Ouvrez le back-office, passez d’un club à l’autre et accédez à chaque section depuis le menu de gauche. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/getting-started Le [back-office](/fr/guide#back-office) est le site que l’équipe de votre club utilise tous les jours. Cette page montre comment vous connecter, comment l’écran est organisé et comment changer de club, de langue ou d’affichage. ## 1. Se connecter [#1-se-connecter] Ouvrez l’adresse du back-office que votre club vous a donnée. Saisissez l’**E-mail** et le **Mot de passe** de votre compte d’équipe, puis sélectionnez **Se connecter**. <figure> <img src="/images/guide/fr/staff/sign-in.png" alt="Page de connexion à Champagne avec les champs E-mail et Mot de passe, un bouton Se connecter et un lien Mot de passe oublié ?." width="1280" height="800" /> <figcaption> La page de connexion, remplie avec une adresse d’exemple. La commande à utiliser est entourée en orange. </figcaption> </figure> > Votre administrateur vous envoie un e-mail d’invitation. Ouvrez le lien de cet e-mail pour choisir > votre nom et votre mot de passe. Voir [Inviter votre équipe](/fr/guide/admins/invite-your-team). Mot de passe oublié ? Sélectionnez **Mot de passe oublié ?**, saisissez votre e-mail et suivez le lien reçu pour en choisir un nouveau. <figure> <img src="/images/guide/fr/staff/forgot-password.png" alt="Page Mot de passe oublié avec un champ E-mail et un bouton Envoyer le lien de réinitialisation." width="448" height="244" /> <figcaption> La page qui vous envoie un lien de réinitialisation. </figcaption> </figure> <figure> <img src="/images/guide/fr/staff/reset-password.png" alt="Page Choisissez un nouveau mot de passe avec les champs Nouveau mot de passe et Confirmer le mot de passe, et un bouton Enregistrer le mot de passe." width="448" height="396" /> <figcaption> La page qu’ouvre le lien reçu par e-mail. </figcaption> </figure> ## 2. Découvrir l’écran [#2-découvrir-lécran] Une fois que vous êtes connecté, Champagne ouvre la première section à laquelle vous avez accès. <figure> <img src="/images/guide/fr/staff/back-office.png" alt="Back-office avec le menu de gauche où Personnes est sélectionné, le nom du club Ukab Clubs en haut et une liste de membres." width="1280" height="800" /> <figcaption> La section Personnes du club d’exemple Ukab Clubs. Les numéros renvoient à la liste ci-dessous. </figcaption> </figure> 1. Le menu de gauche liste les sections que vous pouvez ouvrir, comme **Personnes**, **Réservations**, **Conversations** ou **Finances**. **Mon compte** se trouve en bas, sous **Équipe** pour les administrateurs. 2. Le nom du club en haut à gauche indique le club sur lequel vous travaillez, quand votre compte donne accès à plusieurs clubs. 3. Le nom de la section, juste à côté, vous rappelle où vous êtes. 4. **EN** ou **FR** en haut à droite indique la langue de l’interface. Sélectionnez-le pour passer à l’autre langue. 5. L’interrupteur à côté active ou désactive le mode sombre. **Aide**, avant la langue, ouvre la page de ce guide qui explique l’écran où vous êtes. Sur téléphone, il se trouve en bas du menu. Vous ne voyez que les sections que votre [rôle](/fr/guide#role) autorise. Un coach peut voir **Agenda du club**, **Mes réservations** et **Mes disponibilités**, tandis qu’un responsable voit aussi **Personnes** ou **Finances**. Demandez à votre administrateur si une section dont vous avez besoin manque : [Qui voit quoi](/fr/guide/admins/who-sees-what) indique ce que chaque autorisation affiche. ## 3. Changer de club [#3-changer-de-club] Si vous travaillez pour plusieurs clubs, sélectionnez le nom du club en haut à gauche et choisissez un autre club dans la liste. Une coche indique le club sur lequel vous travaillez. Chaque section affiche alors les membres, les réservations et les paramètres de ce club. <figure> <img src="/images/guide/fr/staff/club-switcher.png" alt="Menu des clubs ouvert sous Ukab Clubs, avec Astel, Ukab Clubs coché et Vosges." width="1280" height="800" /> <figcaption> Le menu des clubs d’un compte qui travaille pour trois clubs d’exemple. </figcaption> </figure> ## Étapes suivantes [#étapes-suivantes] [Gérer les membres](https://docs.champagne.dev-ukab.fr/fr/guide/staff/people) [Gérer les réservations](https://docs.champagne.dev-ukab.fr/fr/guide/staff/reservations) [Répondre aux messages](https://docs.champagne.dev-ukab.fr/fr/guide/staff/chats) # Entretien Suivez et terminez les tâches de ménage et de préparation. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/housekeeping **Entretien** aide l’équipe à garder les chambres prêtes entre deux séjours. Pour chaque chambre, vous pouvez cocher la liste de contrôle du ménage, indiquer son état, facturer ce que le client a consommé et revoir ce qui a été enregistré. La section apparaît quand votre [rôle](/fr/guide#role) comprend l’entretien et qu’une des [catégories de réservation](/fr/guide#reservation-category) de votre club, comme les suites d’hôtel, utilise ce suivi. ## 1. Voir les chambres qui demandent votre attention [#1-voir-les-chambres-qui-demandent-votre-attention] Ouvrez **Entretien** dans le menu de gauche. Les chambres sont regroupées par catégorie de réservation. Chaque carte affiche le dernier état signalé de la chambre et le nombre de points de la liste encore à faire. <figure> <img src="/images/guide/fr/staff/housekeeping-board.png" alt="Page Entretien avec la catégorie Suite d'hôtel et quatre cartes de chambre, Courtyard suite, Garden suite, Penthouse et Terrace suite, chacune marquée État non renseigné avec 6 points restants, Garden suite entourée en orange." width="1280" height="800" /> <figcaption> Les quatre suites d’hôtel d’exemple de la catégorie Suite d'hôtel. La commande à utiliser est entourée en orange. </figcaption> </figure> **État non renseigné** signifie que personne n’a encore indiqué l’état de la chambre. Sélectionnez une carte pour ouvrir le panneau de la chambre. ## 2. Cocher la liste de contrôle [#2-cocher-la-liste-de-contrôle] L’onglet **Liste de contrôle** regroupe les tâches sous **Chaque jour** et **Après chaque réservation**. Le compteur à côté de chaque groupe indique combien de tâches sont faites. Cochez une tâche quand elle est faite : elle est enregistrée tout de suite et barrée. L’icône d’information à côté d’une tâche cochée indique qui l’a cochée et quand. <figure> <img src="/images/guide/fr/staff/housekeeping-checklist.png" alt="Panneau d’entretien de la Garden suite sur l’onglet Liste de contrôle, avec trois tâches Chaque jour, comme Air the room, et trois tâches Après chaque réservation, comme Change the sheets, aucune cochée." width="1280" height="800" /> <figcaption> La liste de contrôle de la chambre d’exemple Garden suite. </figcaption> </figure> > Les membres de l’équipe qui configurent les ressources de réservation modifient la liste depuis > les réglages de la chambre. Voir [Configurer la liste de > contrôle](#5-configurer-la-liste-de-contrôle). ## 3. Indiquer l’état de la chambre [#3-indiquer-létat-de-la-chambre] Ouvrez l’onglet **État** et sélectionnez l’état qui correspond à la chambre : **Propre**, **À nettoyer**, **Maintenance requise** ou **Hors service**. Votre choix est enregistré dès que vous le sélectionnez, et la carte de la chambre l’affiche. <figure> <img src="/images/guide/fr/staff/housekeeping-state.png" alt="Onglet État du panneau de la Garden suite avec les quatre états Propre, À nettoyer, Maintenance requise et Hors service, et un champ Note vide." width="1280" height="800" /> <figcaption> Les choix d’état pour la chambre d’exemple Garden suite. </figcaption> </figure> Ajoutez des détails dans **Note**, par exemple une lampe cassée. La note est enregistrée avec l’état choisi quand vous cliquez en dehors du champ. Sur une chambre dont l’état n’a jamais été signalé, aucun état n’est encore choisi : la note est enregistrée quand vous en choisissez un. ## 4. Facturer les consommations et consulter l’historique [#4-facturer-les-consommations-et-consulter-lhistorique] **Consommation** facture au séjour du client les produits qu’il a consommés, comme les boissons du minibar. 1. Dans **Réservation à facturer**, choisissez le séjour du client. 2. Saisissez la quantité de chaque produit consommé depuis le dernier relevé. 3. Vérifiez le montant en [crédits](/fr/guide#credits) affiché au-dessus du bouton, puis sélectionnez **Signaler et facturer la consommation**. Seuls les séjours confirmés ou terminés d’un membre, déjà commencés et non payés par carte, peuvent être facturés. Le montant est prélevé sur ses crédits : un solde insuffisant refuse la facturation. Sans séjour de ce type, l’onglet affiche **Aucun séjour à facturer**. **Historique** liste chaque état, note et consommation enregistrés pour la chambre. ## 5. Configurer la liste de contrôle [#5-configurer-la-liste-de-contrôle] La liste de contrôle de chaque chambre se modifie dans la section **Réservations**, si votre rôle vous permet de configurer les ressources de réservation. 1. Ouvrez **Réservations** et sélectionnez la catégorie, comme **Suite d'hôtel**. 2. Sélectionnez le nom de la chambre au début de sa ligne dans l’agenda. Vous pouvez aussi l’ouvrir depuis **Paramètres de la catégorie**, dans la liste des ressources. Le panneau de la chambre s’ouvre avec les mêmes onglets que dans **Entretien**, plus **Ressource** et **Produits**. 3. Ouvrez l’onglet **Liste de contrôle** et sélectionnez **Modifier la liste de tâches**. 4. Modifiez une tâche en changeant son texte. Sélectionnez **Ajouter une tâche** sous **Chaque jour** ou **Après chaque réservation** pour en ajouter une, et la croix à côté d’une tâche pour la retirer. Une fenêtre vous demande de confirmer avec **Retirer l’article**. 5. Sélectionnez **Enregistrer la liste de tâches**. Les tâches vides sont ignorées, et la nouvelle liste apparaît dans **Entretien** pour tout le monde. <figure> <img src="/images/guide/fr/staff/housekeeping-edit-checklist.png" alt="Panneau de la Garden suite sur l’onglet Liste de contrôle en mode modification, avec trois tâches Chaque jour et trois tâches Après chaque réservation dans des champs modifiables, chacune avec une croix de suppression, des boutons Ajouter une tâche, et les boutons Annuler et Enregistrer la liste de tâches." width="1280" height="800" /> <figcaption> Modification de la liste de contrôle de la chambre d’exemple Garden suite. </figcaption> </figure> **Annuler** laisse la liste de contrôle telle qu’elle était. Les tâches retirées ne disparaissent qu’à l’enregistrement. # Gérer les plans et les crédits d’un membre Attribuez ou retirez le plan d’un membre et lisez l’historique de ses crédits. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/member-plans-and-credits Les cartes **Adhésions** et **Crédits**, dans l’onglet **Adhésion** d’un profil, montrent ce qu’un membre paie et ce qu’il dépense. Cette page montre comment attribuer un [plan](/fr/guide#plan) à la main, en retirer un et lire chaque mouvement de [crédits](/fr/guide#credits). Pour ouvrir un profil, voir [Travailler sur le profil d’un membre](/fr/guide/staff/member-profile). ## 1. Attribuer ou retirer un plan [#1-attribuer-ou-retirer-un-plan] La carte **Adhésions** liste les plans de la personne. Chaque plan indique **Adhésion valide** ou **Adhésion expirée**, s’il s’agit d’un **Abonnement** ou d’une **Option**, la mention **Payée** ou **Impayée** quand le paiement est connu, et sa date de fin, ou **Sans date de fin**. ### Attribuer un plan [#attribuer-un-plan] Utilisez cette option pour offrir un plan à la main, par exemple un abonnement de saison gratuit. 1. Sélectionnez **Gérer les plans**. 2. Choisissez le **Plan**. 3. Si besoin, indiquez une **Date d’expiration (facultative)**. Laissez-la vide pour un plan sans date de fin. 4. Si besoin, ajoutez un **Motif (facultatif)** pour l’historique. 5. Sélectionnez **Attribuer un plan**. <figure> <img src="/images/guide/fr/staff/grant-plan.png" alt="Fenêtre Gérer les plans avec l’abonnement Adhésion 51 IÉNA choisi, une date d’expiration au 30 juin 2027, un motif d’exemple et les boutons Fermer et Attribuer un plan, Attribuer un plan entouré." width="448" height="408" /> <figcaption> L’attribution d’un abonnement de saison d’exemple à Alex Morgan jusqu’à fin juin 2027. La commande à utiliser est entourée en orange. </figcaption> </figure> Un plan attribué ne crée pas de facture. Si la personne a déjà le même plan attribué à la main, Champagne met à jour sa date d’expiration. ### Retirer un plan [#retirer-un-plan] Sélectionnez **Retirer l’adhésion** sur un plan, ajoutez un **Motif (facultatif)** et confirmez avec **Retirer l’adhésion**. Un plan attribué à la main disparaît immédiatement. Un plan payé via Stripe peut revenir à la prochaine mise à jour envoyée par Stripe : pour arrêter un abonnement payant, résiliez-le dans votre compte de paiement. Ouvrez **Détails techniques** sur un plan pour voir son **Tarif Stripe** et son **Identifiant de l’adhésion**, par exemple quand votre fournisseur Champagne vous les demande. Chaque plan ajouté, renouvelé ou retiré est conservé dans l’**Historique des adhésions** du profil. Voir [Lire l’historique et les documents signés](/fr/guide/staff/member-profile#4-lire-lhistorique-et-les-documents-sign%C3%A9s). ## 2. Lire l’historique des crédits [#2-lire-lhistorique-des-crédits] Sélectionnez **Voir l’historique** dans la carte **Crédits** pour ouvrir l’**Historique des crédits**. Chaque ligne indique à quoi les crédits ont servi, comme une **Recharge**, une **Réservation**, une **Caution**, un **Remboursement**, une **Caution restituée** ou une **Option**, avec sa date et le nombre de crédits ajoutés ou dépensés. Pour une recharge, le montant payé par le membre apparaît sous les crédits. <figure> <img src="/images/guide/fr/staff/member-credit-history.png" alt="Fenêtre Historique des crédits d’Alex Morgan listant deux réservations de transat à -30 Stars, une réservation Pilates with Sarah à -80 Stars et son remboursement à +80 Stars, et une recharge Weekend de +600 Stars payée 50,00 €." width="512" height="446" /> <figcaption> L’historique des crédits du membre d’exemple Alex Morgan. </figcaption> </figure> # Travailler sur le profil d’un membre Repérez-vous dans le profil d’un membre et lisez sa présence, son activité et son historique. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/member-profile Le profil d’un membre rassemble tout ce que votre club sait de lui : ses informations, ses [plans](/fr/guide#plan), ses [crédits](/fr/guide#credits), son statut, ses tags, sa présence, l’activité de son compte et son historique. Cette page explique comment le profil est organisé et comment lire l’activité de la personne. Pour trouver d’abord une personne, voir [Gérer les membres](/fr/guide/staff/people). Les tâches que vous réalisez depuis le profil ont leurs propres pages : * [Modifier le statut d’un membre](/fr/guide/staff/member-status) : suspendre un membre, bloquer ses réservations, le bannir ou le désactiver, et lui rendre l’accès. * [Gérer les plans et les crédits d’un membre](/fr/guide/staff/member-plans-and-credits) : attribuer ou retirer un plan et lire l’historique des crédits. * [Tags, notes et notifications](/fr/guide/staff/member-tags-and-notifications) : étiqueter un membre, tenir une note interne et envoyer une notification. * [Tenir une fiche client](/fr/guide/staff/reservation-people#4-tenir-une-fiche-client) : noter les préférences et les contre-indications pour des catégories comme les soins de beauté. ## 1. Se repérer dans le profil [#1-se-repérer-dans-le-profil] Sélectionnez une personne dans **Personnes** pour ouvrir son profil. Le haut de la page affiche son nom, son statut, ses tags et son e-mail, avec des raccourcis pour **Ouvrir la candidature**, démarrer une **Nouvelle conversation** et **Envoyer une notification**. <figure> <img src="/images/guide/fr/staff/person-profile.png" alt="Profil d’Alex Morgan avec le statut Actif, les tags Founding member et Tournament players, les boutons Ouvrir la candidature, Nouvelle conversation et Envoyer une notification, les onglets Profil, Adhésion, Activité et Historique, et l’onglet Profil avec les cartes Coordonnées personnelles, Statut juridique, Statut du membre et Tags." width="1280" height="800" /> <figcaption> Le profil du membre d’exemple Alex Morgan. Les numéros renvoient aux onglets du tableau ci-dessous. </figcaption> </figure> Le profil est organisé en quatre onglets, qui contiennent chacun des cartes : | # | Onglet | Carte | Ce que vous pouvez faire | | - | -------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | **Profil** | **Coordonnées personnelles** | Voir le nom, le téléphone, la date de naissance, le genre et l’adresse. Sélectionnez **Modifier les informations** pour les changer. | | | | **Statut juridique** | Sélectionnez **Modifier le statut juridique** pour indiquer que la personne est un **Particulier** ou une **Entreprise**. Une entreprise a un **SIRET** d’exactement 14 chiffres et un **Numéro de TVA** facultatif. | | | | **Informations complémentaires** | Répondre aux questions supplémentaires de votre club. Sélectionnez **Modifier les attributs**. La carte n’apparaît que si votre club a des questions supplémentaires. | | | | **Statut du membre** | Si la personne a un accès normal. Voir [Modifier le statut d’un membre](/fr/guide/staff/member-status). | | | | **Tags** | Les étiquettes que votre équipe a mises sur la personne. Voir [Tags, notes et notifications](/fr/guide/staff/member-tags-and-notifications). | | | | **Parrainage** | Qui a parrainé la personne, et qui elle a parrainé. | | | | **Invités** | Les invités du membre, s’il en a. | | | | **Note** | Une note interne sur la personne. Sélectionnez **Modifier la note** pour la changer. | | 2 | **Adhésion** | **Adhésions** | Voir les plans de la personne, attribuer un plan ou en retirer un. Voir [Gérer les plans et les crédits d’un membre](/fr/guide/staff/member-plans-and-credits). | | | | **Documents signés** | Les documents que la personne a signés pendant sa candidature. Voir [l’étape 4](#4-lire-lhistorique-et-les-documents-signés). | | | | **Crédits** | Voir le solde de crédits. Sélectionnez **Voir l’historique** pour voir chaque recharge, réservation, caution, remboursement et option. | | 3 | **Activité** | **Présence** | La fréquence à laquelle la personne a été présente, en retard ou absente. Voir [l’étape 2](#2-vérifier-la-présence). | | | | **Fiche client** | Affichée pour les catégories qui tiennent des fiches clients. Voir [Tenir une fiche client](/fr/guide/staff/reservation-people#4-tenir-une-fiche-client). | | | | **Entrées dans le bâtiment** | Les dernières fois où la personne a signalé son arrivée au club. Voir [l’étape 3](#3-suivre-lactivité-du-compte). | | | | **Activité du compte** | La dernière connexion et si le compte est activé. | | 4 | **Historique** | **Historique des adhésions**, **Historique des statuts** et **Historique du profil** | Chaque changement de plan, de statut et de profil. Voir [l’étape 4](#4-lire-lhistorique-et-les-documents-signés). | Chaque onglet a sa propre adresse : un lien que vous partagez ouvre le même onglet. > Les cartes et les boutons que vous voyez dépendent de vos autorisations. Par exemple, **Statut du > membre** n’apparaît que si vous pouvez modifier le statut d’un membre, et **Gérer les plans** > seulement si vous pouvez gérer les adhésions. Voir [Qui voit quoi](/fr/guide/admins/who-sees-what). ## 2. Vérifier la présence [#2-vérifier-la-présence] Dans l’onglet **Activité**, la carte **Présence** compte les réservations de la personne marquées **Présent**, **En retard** et **Absent** sur les **30 derniers jours**, les **90 derniers jours** ou les **365 derniers jours**. Un lien comme **2 non renseigné(s)** compte les réservations que personne n’a encore marquées. Sélectionnez un nombre ou **Voir les réservations** pour ouvrir la liste. Utilisez le menu en haut pour afficher **Tout** ou un seul état de présence. <figure> <img src="/images/guide/fr/staff/member-attendance.png" alt="Panneau des réservations d’Alex Morgan sur les 90 derniers jours, avec le menu de présence réglé sur Tout et entouré, listant une réservation Place à la piscine marquée Présent." width="1280" height="800" /> <figcaption> Les réservations passées d’Alex Morgan sur les 90 derniers jours. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 3. Suivre l’activité du compte [#3-suivre-lactivité-du-compte] ### Entrées dans le bâtiment [#entrées-dans-le-bâtiment] Quand un membre arrive, il peut sélectionner **Je suis dans le bâtiment** dans son [espace membre](/fr/guide#member-app). Dans l’onglet **Activité**, la carte **Entrées dans le bâtiment** liste la date et l’heure de ses 20 entrées les plus récentes, ou **Aucune entrée pour le moment.** <figure> <img src="/images/guide/fr/staff/member-building-entries.png" alt="Carte Entrées dans le bâtiment listant quatre dates et heures d’arrivée." width="380" height="215" /> <figcaption> Les quatre dernières arrivées du membre d’exemple Alex Morgan. </figcaption> </figure> ### Activité du compte [#activité-du-compte] Dans l’onglet **Activité**, la carte **Activité du compte** indique si le membre peut se connecter : * **Activation en attente** : le membre n’a pas encore choisi de mot de passe. La carte indique si un lien d’activation est en attente, expiré ou valable jusqu’à une date donnée. Sélectionnez **Envoyer l’e-mail d’activation** ou **Renvoyer l’e-mail d’activation** pour lui envoyer un nouveau lien. * Une fois le compte activé, la carte affiche la **Dernière connexion** et **Session active** ou **Déconnecté**. Sélectionnez **Déconnecter le membre** et confirmez pour le déconnecter de tous ses appareils, par exemple après la perte d’un téléphone. Il peut se reconnecter à tout moment. <figure> <img src="/images/guide/fr/staff/member-account-activity.png" alt="Carte Activité du compte avec la dernière connexion, une étiquette Session active et un bouton Déconnecter le membre entouré en orange." width="380" height="184" /> <figcaption> L’activité du compte du membre d’exemple Alex Morgan, qui est connecté. La commande à utiliser est entourée en orange. </figcaption> </figure> ### Parrainage et invités [#parrainage-et-invités] Dans l’onglet **Profil**, la carte **Parrainage** liste les **Parrains** de la personne et les **Membres parrainés** qu’elle a fait venir. Un parrain est enregistré quand votre équipe accepte une [candidature](/fr/guide#application) qui a utilisé son code de parrainage. Sélectionnez un nom pour ouvrir ce profil. Quand le membre a invité des personnes, une carte **Invités** liste chaque invité avec le début de son accès et **Actif**, **Terminé** ou **Révoqué**. <figure> <img src="/images/guide/fr/staff/member-invited-guests.png" alt="Carte Invités listant Clara Fontaine avec le début de son accès et une étiquette Actif." width="380" height="124" /> <figcaption> L’invitée du membre d’exemple Alex Morgan. </figcaption> </figure> ## 4. Lire l’historique et les documents signés [#4-lire-lhistorique-et-les-documents-signés] Sélectionnez l’onglet **Historique**. Il affiche : * **Historique des adhésions** : chaque plan **Ajouté**, **Mis à jour**, **Renouvelé** ou **Supprimé**, avec qui l’a fait, quand, le plan avant et après, et le motif. * **Historique des statuts** : chaque changement de statut avec ses dates et son motif. * **Historique du profil** : chaque modification des informations de la personne, de ses informations complémentaires, de sa photo, de ses tags ou de sa note, avec qui l’a faite et quand. <figure> <img src="/images/guide/fr/staff/member-history.png" alt="Onglet Historique du profil d’Alex Morgan sélectionné et entouré, avec une entrée de l’Historique des adhésions pour le plan Adhésion 51 IÉNA (2026) - Abonnement Membre 2026 marquée Ajouté le 1er septembre 2026, un Historique des statuts encore vide, et un Historique du profil en dessous." width="1280" height="800" /> <figcaption> L’historique d’Alex Morgan, dont le plan a été attribué le 1er septembre 2026. </figcaption> </figure> Sélectionnez une entrée pour voir son détail. Dans l’**Historique du profil**, une entrée liste chaque champ modifié avec sa valeur avant et après. Dans l’onglet **Adhésion**, la carte **Documents signés** liste les documents que la personne a signés pendant sa candidature, avec leur date de signature. Sélectionnez **PDF** pour en télécharger un. Si la carte indique **Les téléchargements sont indisponibles tant que Yousign n’est pas configuré.**, demandez à votre fournisseur Champagne de mettre en place la signature électronique. ## 5. Invités et clients externes [#5-invités-et-clients-externes] Certains profils appartiennent à des personnes qui ne sont pas membres : * Un *invité* a reçu une invitation pour une durée limitée, par votre équipe ou par un membre. Son profil a une carte **Accès invité** avec **Invité par**, **Début de l’accès** et **Fin de l’accès**. Dans la liste Personnes, il affiche **Compte invité**, puis **Ancien invité** une fois son accès terminé. * Un *client externe* a réservé en ligne sans adhésion. Son profil a une carte **Client externe** avec le début et la fin de son accès, et la liste affiche **Client externe**. <figure> <img src="/images/guide/fr/staff/guest-profile.png" alt="Carte Accès invité avec Invité par Alex Morgan et les dates Début de l’accès et Fin de l’accès." width="380" height="168" /> <figcaption> L’invitée Clara Fontaine, invitée par Alex Morgan. </figcaption> </figure> <figure> <img src="/images/guide/fr/staff/external-customer-profile.png" alt="Carte Client externe avec les dates Début de l’accès et Fin de l’accès." width="380" height="128" /> <figcaption> Le client externe Marc Lefort, qui a réservé une suite en ligne. </figcaption> </figure> Les profils d’invités n’ont pas de carte **Parrainage**. Pour inviter une personne, voir [Gérer les membres](/fr/guide/staff/people#3-inviter-un-invit%C3%A9). # Modifier le statut d’un membre Suspendez un membre, bloquez ses réservations, bannissez-le ou désactivez-le, et rendez-lui l’accès. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/member-status Le statut d’un membre détermine s’il peut venir au club et réserver. Cette page montre comment le modifier depuis la carte **Statut du membre** du profil, comment programmer un changement pour plus tard et comment rendre l’accès. Un statut peut aussi changer seul après des absences, quand votre club a défini des [sanctions d’absence](/fr/guide/admins/club-settings#3-d%C3%A9finir-les-r%C3%A8gles-de-r%C3%A9servation). Pour ouvrir un profil, voir [Travailler sur le profil d’un membre](/fr/guide/staff/member-profile). | Statut | Ce que le membre peut faire | | ------------------------- | ---------------------------------------------------------------------- | | **Actif** | Tout : accès normal. | | **Suspendu** | Rien pendant la période choisie. L’accès revient après la date de fin. | | **Réservations bloquées** | Venir au club, sans faire de réservation pendant la période choisie. | | **Banni** | Rien, jusqu’à ce que votre équipe lève le bannissement. | | **Désactivé** | Rien, jusqu’à ce que votre équipe réactive le compte. | ## 1. Modifier le statut d’un membre actif [#1-modifier-le-statut-dun-membre-actif] 1. Dans la carte **Statut du membre**, sélectionnez **Modifier le statut**. 2. Choisissez **Suspendre**, **Bloquer les réservations**, **Désactiver** ou **Bannir**. 3. Remplissez la fenêtre : * **Suspendre** et **Bloquer les réservations** demandent une date de début et une date de fin. La date de début est aujourd’hui ou plus tard. Vous pouvez ajouter un **Motif (facultatif)**. * **Bannir** demande un **Motif**. Le bannissement commence immédiatement. * **Désactiver** demande seulement une confirmation. 4. Sélectionnez le bouton en bas de la fenêtre, par exemple **Suspendre le membre**. <figure> <img src="/images/guide/fr/staff/member-suspend.png" alt="Fenêtre de suspension d’Alex Morgan avec une date de début de suspension aujourd’hui, une date de fin au 30 novembre 2026, un motif d’exemple et les boutons Annuler et Suspendre le membre, Suspendre le membre entouré." width="576" height="456" /> <figcaption> La suspension du membre d’exemple Alex Morgan jusqu’à fin novembre. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 2. Programmer une suspension ou un blocage des réservations [#2-programmer-une-suspension-ou-un-blocage-des-réservations] Quand la date de début est dans le futur, la carte indique **Suspension programmée à partir du** ou **Blocage des réservations programmé à partir du** avec les dates. Le membre garde un accès normal jusque-là. Sélectionnez **Annuler la suspension programmée** ou **Annuler le blocage des réservations programmé** pour y renoncer. ## 3. Rendre l’accès [#3-rendre-laccès] Tant qu’un statut est en vigueur, la carte affiche ses dates ou son motif, et un bouton pour y mettre fin tout de suite : | Statut | Bouton | | ------------------------- | ------------------------------------------ | | **Suspendu** | **Mettre fin à la suspension** | | **Réservations bloquées** | **Mettre fin au blocage des réservations** | | **Banni** | **Lever le bannissement** | | **Désactivé** | **Réactiver le membre** | Le bouton rend le membre **Actif** immédiatement, sans fenêtre de confirmation. <figure> <img src="/images/guide/fr/staff/member-status-suspended.png" alt="Carte Statut du membre indiquant une suspension du 1er septembre 2026 au 31 décembre 2026, avec le motif et un bouton Mettre fin à la suspension entouré." width="380" height="224" /> <figcaption> Le statut de Camille Roussel, membre d’exemple suspendue jusqu’à la fin de l’année. </figcaption> </figure> Chaque changement de statut est conservé dans l’**Historique des statuts** du profil. Voir [Lire l’historique et les documents signés](/fr/guide/staff/member-profile#4-lire-lhistorique-et-les-documents-sign%C3%A9s). # Tags, notes et notifications Étiquetez un membre avec des tags, tenez une note interne à son sujet et envoyez-lui une notification. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/member-tags-and-notifications Depuis le profil d’un membre, vous pouvez l’étiqueter avec des tags, écrire une note pour votre équipe et lui envoyer une courte notification. Cette page montre chacune de ces tâches. Pour ouvrir un profil, voir [Travailler sur le profil d’un membre](/fr/guide/staff/member-profile). ## 1. Étiqueter un membre avec des tags [#1-étiqueter-un-membre-avec-des-tags] Les tags sont des étiquettes que seule votre équipe voit, comme « Juniors » ou « Tournament players ». Utilisez-les pour retrouver des personnes dans la liste et pour choisir les destinataires d’une [lettre d’information](/fr/guide/staff/newsletters). Sélectionnez **Modifier les tags** dans la carte **Tags**. Cochez un tag pour l’ajouter, décochez-le pour le retirer. Chaque changement est enregistré aussitôt. Vous pouvez aussi sélectionner la croix d’un tag dans la carte pour le retirer. <figure> <img src="/images/guide/fr/staff/member-tags.png" alt="Menu des tags ouvert sur le profil d’Alex Morgan sous le bouton Modifier les tags entouré, avec un champ de recherche, les tags Founding member et Tournament players cochés, Juniors décoché et un lien Gérer les tags." width="1280" height="800" /> <figcaption> Les trois tags d’exemple d’Ukab Clubs, dont deux sur Alex Morgan. La commande à utiliser est entourée en orange. </figcaption> </figure> Pour créer un tag, saisissez son nom dans **Rechercher ou créer un tag…**. Quand aucun tag ne porte ce nom, sélectionnez **Créer**. La pastille à côté règle la **Couleur du tag** ; choisissez-la avant de créer le tag. Le nouveau tag est ajouté à la personne tout de suite. <figure> <img src="/images/guide/fr/staff/member-tag-create.png" alt="Menu des tags avec Volunteers tapé dans le champ de recherche, un bouton Créer Volunteers entouré et une pastille de couleur rouge." width="289" height="125" /> <figcaption> La création d’un tag d’exemple, Volunteers. </figcaption> </figure> Pour supprimer un tag de tout le club, survolez-le dans le menu et sélectionnez l’icône de corbeille, puis **Supprimer**. Le tag est retiré de toutes les personnes qui le portent. **Gérer les tags**, en bas du menu, ouvre les paramètres des membres, où un administrateur renomme les tags et change leur couleur. **Retour à** suivi du nom de la personne ramène à sa fiche. Ce lien n’apparaît que si vous pouvez ouvrir **Paramètres**. Voir [Paramètres du club](/fr/guide/admins/club-settings). ## 2. Tenir une note [#2-tenir-une-note] La carte **Note** contient une note interne sur la personne, par exemple une habitude utile à connaître à l’accueil. Les membres ne la voient pas. 1. Sélectionnez **Modifier la note** dans la carte **Note**. 2. Saisissez la note, jusqu’à 10 000 caractères. Le compteur sous le champ indique combien vous en avez utilisé. 3. Sélectionnez **Enregistrer la note**, ou **Annuler** pour laisser la note telle quelle. <figure> <img src="/images/guide/fr/staff/member-note.png" alt="Carte Note en cours de modification avec une note d’exemple sur Alex Morgan, un compteur de caractères et les boutons Enregistrer la note et Annuler, Enregistrer la note entouré." width="380" height="263" /> <figcaption> L’écriture d’une note d’exemple sur le profil d’Alex Morgan. </figcaption> </figure> **Modifier la note** n’apparaît que si votre [rôle](/fr/guide#role) a l’autorisation **Modifier les notes internes**. ## 3. Envoyer une notification [#3-envoyer-une-notification] Utilisez une notification pour une courte information pratique, par exemple un colis qui attend à l’accueil. 1. Sélectionnez **Envoyer une notification** en haut du profil. 2. Choisissez une **Catégorie** : **Colis reçu**, **Objet perdu** ou **Autre**. Les deux premières remplissent un **Message** tout prêt que vous pouvez modifier. **Autre** commence vide. 3. Sélectionnez **Envoyer**. <figure> <img src="/images/guide/fr/staff/send-notification.png" alt="Fenêtre Envoyer une notification pour Alex Morgan avec la catégorie Colis reçu et son message prérempli, et les boutons Annuler et Envoyer, Envoyer entouré." width="576" height="424" /> <figcaption> Une notification Colis reçu pour Alex Morgan, avec son message tout prêt. </figcaption> </figure> Le membre la reçoit dans l’application et par e-mail, selon ses réglages de notification **Messages du club**. Voir [Notifications](/fr/guide/members/notifications). # Envoyer des lettres d’information Rédigez une lettre d’information, choisissez qui la reçoit, prévisualisez-la et envoyez-la aux membres. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/newsletters La section **Lettres d’information** vous permet d’écrire un e-mail une seule fois et de l’envoyer à tous vos membres ou à un groupe d’entre eux. Cette page montre comment rédiger une lettre d’information, vérifier son rendu, choisir ses destinataires et l’envoyer. ## 1. Ouvrir la section Lettres d’information [#1-ouvrir-la-section-lettres-dinformation] Dans le menu de gauche, sélectionnez **Lettres d’information**. La liste affiche chaque lettre d’information avec son objet, la date de sa dernière modification ou de son envoi, et son statut. <figure> <img src="/images/guide/fr/staff/newsletters.png" alt="Section Lettres d’information avec un bouton Nouvelle lettre d’information entouré en orange, un avertissement L’envoi n’est pas configuré et une lettre d’information en Brouillon, Autumn tournament: registrations are open, avec sa date de modification et le tag Tournament players." width="1280" height="800" /> <figcaption> La section Lettres d’information du club d’exemple Ukab Clubs, avec un brouillon pour les joueurs du tournoi. Le bouton Nouvelle lettre d’information, entouré en orange, crée un nouveau brouillon. </figcaption> </figure> Sous l’objet, chaque ligne affiche les tags auxquels la lettre est envoyée. Une lettre d’information destinée à tous les membres n’a pas de tag. | Statut | Ce qu’il signifie | | ------------------ | ---------------------------------------------------------------------------- | | **Brouillon** | La lettre d’information est en cours d’écriture. Seule votre équipe la voit. | | **Envoi en cours** | Les e-mails sont en route. | | **Envoyée** | L’envoi est terminé. La lettre d’information ne peut plus être modifiée. | | **Échec** | L’envoi s’est arrêté. Ouvrez la lettre d’information pour voir pourquoi. | ### Avis sur l’envoi [#avis-sur-lenvoi] Un avis en haut de la liste et de chaque lettre d’information vous signale quand l’envoi demande votre attention : | Avis | Ce qu’il signifie et ce qu’il faut faire | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **L’envoi n’est pas configuré** | L’envoi d’e-mails n’est pas encore activé pour votre installation de Champagne. Vous pouvez quand même écrire des brouillons. Contactez votre fournisseur Champagne. | | **Configurez un domaine d’envoi** | Votre club n’a pas encore d’adresse d’envoi. Un admin du club la configure : sélectionnez **Ouvrir les paramètres d’envoi**. | | **Le domaine d’envoi n’est pas encore vérifié** | L’adresse d’envoi attend sa vérification. Sélectionnez **Ouvrir les paramètres d’envoi** pour la contrôler. Voir [Paramètres du club](/fr/guide/admins/club-settings#envoyer-les-lettres-dinformation-depuis-votre-propre-adresse). | | **Environnement de test** | Cette installation sert aux tests. Les lettres d’information ne partent qu’aux adresses listées dans l’avis, ou **Les lettres d’information ne sont envoyées à personne.** Les membres hors de la liste ne reçoivent rien. | Quand aucun avis n’apparaît, l’envoi est prêt. ## 2. Rédiger une lettre d’information [#2-rédiger-une-lettre-dinformation] Sélectionnez **Nouvelle lettre d’information**. Champagne crée un brouillon et l’ouvre. Pour reprendre un brouillon, sélectionnez-le dans la liste. Le formulaire est à gauche et l’**Aperçu de l’e-mail** à droite. <figure> <img src="/images/guide/fr/staff/newsletter-editor.png" alt="Brouillon de lettre d’information Autumn tournament: registrations are open, avec les Destinataires réglés sur le tag Tournament players et 1 membre abonné, les champs Objet, Texte d’aperçu et Message à gauche et l’e-mail mis en forme à droite." width="1280" height="800" /> <figcaption> Le brouillon d’exemple qui annonce le tournoi d’automne, avec l’aperçu de son e-mail. </figcaption> </figure> 1. Saisissez l’**Objet**, le titre que les membres voient dans leur boîte de réception. 2. Saisissez un **Texte d’aperçu** : la courte ligne que la plupart des messageries affichent après l’objet. 3. Écrivez le **Message**. Utilisez les boutons au-dessus de la zone pour ajouter un **Titre**, du texte en **Gras** ou en **Italique**, un **Lien**, une **Liste à puces** ou une **Liste numérotée**, une **Citation**, un **Séparateur**, une **Image** ou un **Bouton**. 4. Sélectionnez **Enregistrer** en bas du formulaire. L’aperçu se met à jour pendant que vous écrivez et montre l’e-mail tel que les membres le recevront, avec le nom de votre club en haut. ## 3. Choisir les destinataires [#3-choisir-les-destinataires] Par défaut, **Destinataires** est réglé sur **Tous les membres**. Pour n’écrire qu’à un groupe, sélectionnez **Choisir des tags** et cochez un ou plusieurs tags. La lettre d’information part alors aux membres qui ont au moins un de ces tags. Sélectionnez **Ajouter** pour choisir d’autres tags, ou la croix d’un tag dans le champ pour élargir de nouveau le public. <figure> <img src="/images/guide/fr/staff/newsletter-tags.png" alt="Menu des tags ouvert sous Destinataires, avec un champ de recherche, les tags Founding member, Juniors et Tournament players, Tournament players coché, et un lien Gérer les tags." width="1280" height="800" /> <figcaption> Choix des tags de la lettre d’information d’exemple sur le tournoi. La commande à utiliser est entourée en orange. </figcaption> </figure> La ligne sous le champ compte les membres qui la recevront, par exemple **3 membres abonnés**. Seuls les membres avec une adhésion en cours reçoivent les lettres d’information. Les membres désabonnés et les comptes **Banni** ou **Désactivé** sont exclus. Les membres **Suspendu** les reçoivent toujours. > Les tags sont des étiquettes que vous placez sur les fiches des membres dans **Personnes**, par > exemple « Juniors » ou « Tournament players ». Pour en créer un d’ici, saisissez son nom dans > **Rechercher ou créer un tag…** et sélectionnez **Créer** : le tag est ajouté à votre club et à > cette lettre d’information. Voir [Tags, notes et > notifications](/fr/guide/staff/member-tags-and-notifications#1-%C3%A9tiqueter-un-membre-avec-des-tags). ## 4. Tester et envoyer [#4-tester-et-envoyer] Quand l’envoi est configuré et que le brouillon a un objet et un message : 1. Sélectionnez **Envoyer un test** pour recevoir d’abord la lettre d’information vous-même. Cochez jusqu’à cinq adresses de l’équipe de votre club, sélectionnez **Envoyer un test** et vérifiez l’e-mail dans votre boîte de réception. 2. Sélectionnez **Envoyer**. Une fenêtre vous demande de confirmer le nombre de destinataires, par exemple **Envoyer à 3 destinataires ?**. 3. Sélectionnez **Envoyer** dans cette fenêtre. Les modifications non enregistrées sont d’abord enregistrées. > Une fois envoyée, une lettre d’information ne peut plus être modifiée ni rappelée. Après l’envoi, ouvrez la lettre d’information pour suivre sa distribution : combien d’e-mails ont été **Délivrés**, **Rejetés**, **Désabonnés** ou **Signalés comme spam**. **Non envoyés** compte les e-mails qui n’ont pas pu partir. Si l’envoi s’arrête, la lettre d’information affiche **Échec** et un avis **L’envoi a échoué** avec la raison. Vous pouvez alors corriger le problème et sélectionner de nouveau **Envoyer**. Pour supprimer un brouillon dont vous n’avez plus besoin, ouvrez-le et sélectionnez **Supprimer**. La fenêtre **Supprimer ce brouillon ?** indique que le brouillon sera définitivement supprimé. Sélectionnez **Supprimer le brouillon** pour confirmer, ou **Annuler** pour le garder. ## 5. Quand un collègue modifie le même brouillon [#5-quand-un-collègue-modifie-le-même-brouillon] Si quelqu’un d’autre enregistre le brouillon alors que vous avez des modifications non enregistrées, une fenêtre **Cette lettre d’information a été modifiée** s’ouvre : * **Recharger** affiche sa version. Vos modifications non enregistrées sont perdues. * **Continuer la modification** garde votre texte. Quand vous enregistrez, il remplace sa version. Quand vous n’avez pas de modifications non enregistrées, le brouillon se met à jour tout seul. # Gérer les membres Trouvez un membre, ouvrez son profil et mettez à jour ses informations. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/people La section **Personnes** liste tous les membres et clients de votre club. De là, vous pouvez trouver quelqu’un, ajouter une nouvelle personne, inviter quelqu’un pour quelques heures et ouvrir un profil. ## 1. Trouver une personne [#1-trouver-une-personne] Dans le menu de gauche, sélectionnez **Personnes**. La liste affiche le **Nom**, l’**E-mail** et le **Statut** de chaque personne. Sélectionnez le titre d’une colonne pour trier la liste selon cette colonne. <figure> <img src="/images/guide/fr/staff/people.png" alt="Section Personnes avec un champ de recherche, les boutons Ajouter une personne, Invitations d’invités et Filtres, et une liste de membres avec leur e-mail et un statut comme Actif, Suspendu ou Désactivé." width="1280" height="800" /> <figcaption> Les membres du club d’exemple Ukab Clubs, triés par nom. </figcaption> </figure> Un membre actif qui n’a pas encore choisi de mot de passe affiche **Activation en attente**. Certaines personnes affichent aussi une seconde étiquette à côté de leur statut : | Étiquette | De qui il s’agit | | ------------------ | ------------------------------------------------------------------------ | | **Compte invité** | Un invité accueilli pour quelques heures, dont l’accès est encore actif. | | **Ancien invité** | Un invité dont l’accès est terminé. | | **Client externe** | Une personne qui a réservé en ligne sans adhésion. | Saisissez une partie d’un nom ou d’une adresse e-mail dans **Rechercher par nom ou e-mail…**. La liste se met à jour pendant la saisie. Quand personne ne correspond, sélectionnez **Effacer la recherche et les filtres** pour revoir tout le monde. Pour affiner encore la liste, sélectionnez **Filtres**, choisissez vos critères et sélectionnez **Appliquer les filtres**. Vous pouvez filtrer par : | Filtre | Ce qu’il garde | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **Statut** | Les membres **Actif**, **Suspendu**, **Réservations bloquées**, **Banni** ou **Désactivé**. | | **Adhésion** | Les personnes avec une **Adhésion valide**, **Adhésions expirées uniquement**, **Toute adhésion** ou **Aucun historique d’adhésion**. | | **Type** | Un **Particulier** ou une **Entreprise**. | | **Genre** | Le genre indiqué sur le profil. | | **Âge minimum**, **Âge maximum** | Les personnes dans une tranche d’âge. | | **Tags** | Les personnes qui ont au moins un des tags choisis, ou **Uniquement les personnes sans tag**. | | **Attributs** | Les questions supplémentaires que votre club enregistre, comme une langue préférée ou une profession. | **Tags** et **Attributs** n’apparaissent que si votre club a des tags ou des questions supplémentaires. <figure> <img src="/images/guide/fr/staff/people-filters.png" alt="Fenêtre Filtres avec les champs Statut, Adhésion, Type, Genre, Âge minimum et Âge maximum, un champ Tags, un groupe Attributs avec Preferred language, Printed newsletter et Profession, et les boutons Réinitialiser, Annuler et Appliquer les filtres." width="1280" height="800" /> <figcaption> Les filtres d’Ukab Clubs, dont trois attributs d’exemple. La commande à utiliser est entourée en orange. </figcaption> </figure> Le bouton **Filtres** indique combien de filtres sont actifs. Sélectionnez **Réinitialiser** dans la fenêtre, puis **Appliquer les filtres**, pour tous les retirer. ## 2. Ajouter une personne [#2-ajouter-une-personne] Sélectionnez **Ajouter une personne**, saisissez le **Prénom**, le **Nom** et l’**E-mail**, puis sélectionnez **Créer une personne**. La personne reçoit un e-mail avec un lien pour choisir son mot de passe. <figure> <img src="/images/guide/fr/staff/add-person.png" alt="Fenêtre d’ajout d’une personne remplie avec le prénom Camille, le nom Martin et l’e-mail camille.martin@example.com, avec les boutons Annuler et Créer une personne." width="1280" height="800" /> <figcaption> L’ajout d’un membre d’exemple, Camille Martin. </figcaption> </figure> ## 3. Inviter un invité [#3-inviter-un-invité] Un invité est une personne qui reçoit un compte temporaire pour quelques heures, par exemple un visiteur venu pour un événement. Sélectionnez **Invitations d’invités**. 1. Saisissez l’**E-mail de l’invité**. 2. Choisissez la **Durée d’accès** : **12 heures**, **24 heures** ou **48 heures**. Le délai commence quand l’invité accepte l’invitation. 3. Sélectionnez **Envoyer l’invitation**. L’invité reçoit un e-mail pour l’accepter. <figure> <img src="/images/guide/fr/staff/guest-invitations.png" alt="Fenêtre Invitations d’invités avec l’e-mail guest@example.com, une durée d’accès de 24 heures et une liste Invitations envoyées avec deux invitations en attente et l’accès invité actif de Clara Fontaine." width="1280" height="800" /> <figcaption> L’invitation d’un invité d’exemple pour 24 heures. </figcaption> </figure> La liste **Invitations envoyées**, dans la même fenêtre, affiche chaque invitation avec son état : **Invitation en attente**, **Invitation expirée**, **Accès invité actif** ou **Accès invité terminé**. > **Ajouter une personne** et **Invitations d’invités** n’apparaissent que si votre > [rôle](/fr/guide#role) comprend l’autorisation **Créer et modifier les fiches membres**. Demandez à > votre administrateur si vous en avez besoin, et voyez [Qui voit quoi](/fr/guide/admins/who-sees-what) > pour les autres autorisations. ## 4. Ouvrir le profil d’un membre [#4-ouvrir-le-profil-dun-membre] Sélectionnez une personne dans la liste pour ouvrir son profil. Vous y mettez à jour ses informations, ses [plans](/fr/guide#plan), son statut et ses tags, vous lui envoyez une notification et vous suivez son activité. Voir [Travailler sur le profil d’un membre](/fr/guide/staff/member-profile). > Sélectionnez un tag dans la liste Personnes pour n’afficher que les personnes qui l’ont. Sélectionnez **Retour à Personnes** pour revenir à la liste, avec votre recherche et vos filtres inchangés. # Utiliser l’agenda du club Lisez la journée d’un coup d’œil, gérez les séances et travaillez avec les plans des espaces. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/planning **Agenda du club** est l’agenda des [catégories de réservation](/fr/guide#reservation-category) dans lesquelles vous travaillez, comme les cours collectifs, les cours particuliers ou les transats de la piscine. Il vous permet de voir qui a réservé, de suivre les places restantes de chaque séance, de noter les présences, de gérer une séance et de trouver une place libre sur un plan des espaces. Votre administrateur choisit les catégories qui apparaissent ici. ## 1. Lire la semaine ou la journée [#1-lire-la-semaine-ou-la-journée] Ouvrez **Agenda du club** dans le menu de gauche et sélectionnez une catégorie dans la barre du haut. <figure> <img src="/images/guide/fr/staff/planning-week.png" alt="Agenda du club pour la catégorie Cours collectif en vue Semaine : la ligne Main studio montre chaque jour une séance de 09:00 marquée Passé ou Séance complète, une séance Open studio à 12:30 et un lien +2 séances." width="1280" height="800" /> <figcaption> Une semaine des cours collectifs d’exemple Cours collectif dans le Main studio, avec l’Open studio quotidien. La commande à utiliser est entourée en orange. </figcaption> </figure> * **Semaine** affiche sept jours côte à côte. **Jour** affiche les heures d’une seule journée. Sélectionnez le nom d’un jour dans l’en-tête pour ouvrir cette journée. * Les flèches passent à la période précédente ou suivante, et l’icône de calendrier vous ramène à aujourd’hui. * **Rechercher...** trouve une réservation par nom. * Dans les cours collectifs, chaque bloc est une séance. Il affiche son heure de début et **Séance complète**, le nombre de places restantes, ou **Passé** une fois la séance terminée. Une séance qui a son propre nom, comme **Open studio**, affiche ce nom. En vue **Semaine**, chaque jour affiche ses deux premières séances sous forme de cartes. Quand une journée en compte davantage, sélectionnez un lien comme **+2 séances** pour ouvrir cette journée et les voir toutes. Les réservations fonctionnent comme dans la section **Réservations** : sélectionnez-en une pour l’ouvrir, ou survolez un horaire libre et sélectionnez **+** pour en ajouter une si votre [rôle](/fr/guide#role) le permet. Voir [Gérer les réservations](/fr/guide/staff/reservations). ## 2. Suivre une séance et marquer la présence [#2-suivre-une-séance-et-marquer-la-présence] Sélectionnez une séance pour ouvrir son panneau. **Places réservées** montre son remplissage quand la séance a une limite, et **Participants** liste qui vient et combien de places chaque personne a réservées. <figure> <img src="/images/guide/fr/staff/planning-session.png" alt="Panneau de la séance Cours collectif de demain, de 09:00 à 10:00 dans le Main studio, marquée Séance complète, avec 8 places réservées sur 8, six participants dont Louis Garnier avec 3 places, le début de la Liste d’attente et un bouton Ajouter un participant désactivé." width="1280" height="800" /> <figcaption> Le Cours collectif d’exemple de 09:00, complet avec six participants et trois membres en attente. </figcaption> </figure> * À partir du jour de la séance, marquez chaque participant **Présent**, **En retard** ou **Absent**. **En retard** et **Absent** deviennent disponibles dès le début de la séance. **Marquer les restants présents** marque en une fois toutes les personnes que vous n’avez pas encore marquées. * **Ajouter un participant** inscrit un membre à la séance tant qu’il reste des places, jusqu’au début de la séance. Ce bouton nécessite l’autorisation **Créer et modifier les réservations**, et la saisie des présences nécessite **Valider les présences et les billets**. * L’icône en bout de ligne d’un participant le retire de la séance. Sa réservation est annulée et tout paiement est remboursé : une fenêtre de confirmation s’affiche donc d’abord. Cette icône nécessite l’autorisation **Créer et modifier les réservations**. Quand le délai d’annulation est dépassé, la fenêtre propose **Rembourser le prix** : laissez la case décochée pour garder le prix et ne rendre que la caution. * Quand des membres attendent une place, **Liste d’attente** les liste dans l’ordre, avec la date de leur inscription. Une place qui se libère leur revient automatiquement, dans cet ordre. <figure> <img src="/images/guide/fr/staff/planning-waitlist.png" alt="Le même panneau de séance, plus bas : six participants, puis une Liste d’attente de 3 avec Sarah Cohen #1, Adam Diallo #2 et Alex Morgan #3, chacun dans le Main studio avec la date de son inscription." width="1280" height="800" /> <figcaption> Trois membres en attente d’une place dans la séance complète d’exemple. </figcaption> </figure> Les étiquettes sous le titre de la séance affichent **En cours** ou **Passé**, **Séance complète**, et **Accès libre** pour les séances auxquelles on vient sans réserver, comme l’**Open studio** d’exemple. Les séances en accès libre n’ont ni jauge **Places réservées** ni liste de participants. > Quand les membres reçoivent un billet avec QR code pour la catégorie, **Billets** au-dessus de > l’agenda le contrôle, et **Marquer présent** enregistre leur arrivée. Voir [Enregistrer l’arrivée > et le départ des clients](/fr/guide/staff/check-in-and-deposits). ## 3. Modifier, exporter ou annuler une séance [#3-modifier-exporter-ou-annuler-une-séance] Sélectionnez le bouton **⋯** à côté du titre de la séance pour ouvrir **Actions de la séance**. Les choix dépendent de la séance et de votre rôle. <figure> <img src="/images/guide/fr/staff/planning-session-actions.png" alt="Panneau de la séance complète Cours collectif de 09:00 avec le menu Actions de la séance ouvert, qui propose Modifier la séance, Exporter les inscrits, Exporter la liste d’attente et Annuler la séance." width="1280" height="800" /> <figcaption> Les actions de la séance complète d’exemple Cours collectif, vues par un administrateur. </figcaption> </figure> * **Modifier la séance** change les détails de la séance, comme son horaire, son **Lieu** ou sa **Limite de membres (facultative)**. Ce choix apparaît avant le début de la séance, pour l’équipe qui gère la catégorie. Voir [Séances, listes d’invités et périodes bloquées](/fr/guide/admins/sessions-and-blocked-periods). * **Exporter les inscrits**, quand la séance a des participants, télécharge un tableur (fichier CSV) avec chaque participant, le statut de sa réservation, sa présence et son nombre de places. * **Exporter la liste d’attente**, quand des personnes attendent une place, les télécharge aussi dans un fichier CSV. * **Annuler la séance** l’annule pour tout le monde avant son début. Elle nécessite l’autorisation **Créer et modifier les réservations**. Une fenêtre demande **Annuler cette séance ?** : les membres sont prévenus et les réservations payées sont remboursées. Sélectionnez **Annuler la séance** pour confirmer, ou **Conserver la séance** pour la garder. ## 4. Utiliser le plan des espaces [#4-utiliser-le-plan-des-espaces] Certaines catégories, comme les transats de la piscine, ont un plan des espaces. Il apparaît sous l’agenda, sous **Plan des espaces**. Quand une catégorie a plusieurs plans, sélectionnez le nom d’un plan au-dessus du dessin pour en changer. <figure> <img src="/images/guide/fr/staff/planning-floor-plan.png" alt="Vue Jour de la catégorie Place à la piscine avec une ligne verticale de l’heure, et en dessous le plan Piscine extérieure montrant une piscine, des plantes, un parasol et cinq transats nommés Transat #1 à Transat #5." width="1280" height="800" /> <figcaption> Le plan d’exemple de la piscine extérieure, Piscine extérieure, et ses cinq transats. </figcaption> </figure> Le plan montre chaque place à l’heure indiquée par la ligne verticale de l’agenda. Faites glisser cette ligne vers une autre heure pour mettre le plan à jour. Les places réservées ou indisponibles à cette heure apparaissent en gris. Sélectionnez une place libre pour ouvrir le formulaire de réservation pour cette place et cette heure, si votre rôle vous permet d’ajouter des réservations. <figure> <video src="/videos/guide/fr/staff/planning-time-cursor.webm" aria-label="Le curseur de l’heure est glissé jusqu’à 17:45, sur la réservation de Louis Garnier : Transat #3 passe en gris sur le plan en dessous, puis le curseur revient en arrière." width="835" height="640" /> <figcaption> Placé sur une réservation, le curseur de l’heure fait passer son transat en gris sur le plan. </figcaption> </figure> > L’équipe qui gère la catégorie peut sélectionner **Modifier le plan** pour organiser l’agencement. > Le guide [Les outils d’IA pour votre plan des espaces](/fr/ai-flows) explique comment [importer un > plan existant](/fr/ai-flows/plan-import) et [créer de nouveaux objets](/fr/ai-flows/artwork-generation). # Personnes, produits et fiches clients d’une réservation Invitez quelqu’un à une réservation, marquez la présence, ajoutez des produits et tenez une fiche client. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/reservation-people La page d’une réservation rassemble ce qu’il faut savoir sur le client : les personnes inscrites sur la réservation, s’il est venu ou non, les produits commandés et, pour certaines catégories, sa fiche client. Cette page montre comment travailler avec chacun de ces éléments. Pour ouvrir une réservation, voir [Gérer les réservations](/fr/guide/staff/reservations#4-ouvrir-d%C3%A9placer-ou-annuler-une-r%C3%A9servation). **Inviter** et **Supprimer** nécessitent l’autorisation **Créer et modifier les réservations**. Les marques de présence nécessitent **Valider les présences et les billets**. Quand votre [rôle](/fr/guide#role) n’a pas l’une de ces autorisations, les boutons correspondants n’apparaissent pas. ## 1. Gérer les personnes sur une réservation [#1-gérer-les-personnes-sur-une-réservation] **Personnes sur cette réservation** liste le **Contact de la réservation** et les personnes qu’il a invitées, avec le nombre de personnes par rapport aux places réservées. Chaque personne invitée est marquée **Invité**, **Acceptée** ou **Refusé**. **Compte associé** signifie que l’e-mail appartient à un membre de votre club. * Pour inviter quelqu’un, sélectionnez **Inviter**, saisissez son **E-mail** et sélectionnez **Envoyer l’invitation**. La personne reçoit un e-mail pour accepter ou refuser. Quand votre club permet aux invités d’utiliser un compte temporaire, choisissez sa **Durée d’accès** : 12, 24 ou 48 heures. Le compte temporaire démarre quand l’invité accepte. * Pour retirer quelqu’un de la réservation, sélectionnez **Supprimer** sur sa ligne. <figure> <img src="/images/guide/fr/staff/invite-someone.png" alt="Fenêtre Inviter quelqu’un avec friend@example.com dans le champ E-mail, un avertissement indiquant que la réservation est complète et qu’inviter quelqu’un y ajoute une personne, et les boutons Fermer et Envoyer l’invitation, Envoyer l’invitation entouré." width="384" height="292" /> <figcaption> L’invitation d’un ami à la réservation d’exemple d’Alex Morgan sur Transat #4, déjà complète. La commande à utiliser est entourée en orange. </figcaption> </figure> Quand toutes les places sont prises, la fenêtre vous prévient que l’invitation ajoute une personne à la réservation. ## 2. Marquer la présence [#2-marquer-la-présence] **Présence** indique si le client est venu. Sélectionnez **Présent**, **En retard** ou **Absent**. **En retard** et **Absent** deviennent disponibles une fois la réservation commencée. Pour joindre une note à la marque, saisissez-la dans **Ajouter une note (facultatif)** avant de choisir la marque. La dernière marque indique qui l’a enregistrée et quand. Les marques s’additionnent dans la carte **Présence** du profil du membre. Voir [Vérifier la présence](/fr/guide/staff/member-profile#2-v%C3%A9rifier-la-pr%C3%A9sence). > **Absent** compte comme une absence non excusée. Quand votre club a des sanctions d’absence, assez > d’absences avertissent, suspendent, bloquent ou bannissent le membre aussitôt, et il en est > prévenu. Vérifiez la marque avant de l’enregistrer. Voir [Définir les règles de > réservation](/fr/guide/admins/club-settings#3-d%C3%A9finir-les-r%C3%A8gles-de-r%C3%A9servation). ## 3. Ajouter des produits à une réservation [#3-ajouter-des-produits-à-une-réservation] Quand la ressource propose des produits, comme des boissons, le formulaire **Ajouter une réservation** les liste sous **Produits** à droite. Utilisez **+** et **−** pour régler la quantité de chacun. **Total des produits** indique leur coût. Les produits marqués **Réservé au personnel** sont cachés aux membres. <figure> <img src="/images/guide/fr/staff/reservation-products.png" alt="Formulaire Ajouter une réservation pour le transat Transat #2 avec une colonne Produits listant Sparkling water à 3,5 Stars, une quantité de 2 avec son bouton + entouré, et un Total des produits de 7 Stars." width="896" height="604" /> <figcaption> Deux bouteilles de Sparkling water d’exemple ajoutées à la réservation d’un transat de la piscine. </figcaption> </figure> **Produits facturés** apparaît sur la page de la réservation quand des produits ont été ajoutés à la réservation, comme des boissons du minibar facturées depuis **Entretien**. Chaque ligne indique le produit, la quantité et le montant. <figure> <img src="/images/guide/fr/staff/reservation-product-charges.png" alt="Carte Produits facturés listant Welcome champagne × 1 à 65 Stars, Sparkling water × 2 à 7 Stars et Champagne bottle × 1 à 65 Stars." width="1044" height="162" /> <figcaption> Les produits facturés pendant le séjour d’exemple de Jamie Chen dans la Garden suite. </figcaption> </figure> ## 4. Tenir une fiche client [#4-tenir-une-fiche-client] Certaines [catégories de réservation](/fr/guide#reservation-category), comme les soins de beauté, tiennent une fiche client. Votre administrateur active cette option par catégorie. La carte **Fiche client** apparaît alors sur chaque réservation de cette catégorie et dans l’onglet **Activité** du profil de chaque personne qui y a réservé, avec un onglet par catégorie quand il y en a plusieurs. La fiche affiche : * **Contre-indications**, surlignées en rouge quand elles sont remplies, comme une allergie. * **Préférences** et une **Note**. * **Visites** : le nombre de visites passées et les plus récentes, avec leur date, leur heure, leur ressource et la présence. Sélectionnez **Tout afficher** pour en voir jusqu’à dix, et sélectionnez une visite pour ouvrir la réservation. <figure> <img src="/images/guide/fr/staff/customer-sheet.png" alt="Carte Fiche client de la catégorie Soin en salon de beauté sans contre-indication enregistrée, avec Préférences et Note vides, 17 visites dont les trois dernières listées, et un bouton Modifier la fiche entouré." width="645" height="321" /> <figcaption> La fiche client du membre d’exemple Zoé Lefèvre, avec ses soins de beauté passés. </figcaption> </figure> Pour la remplir : 1. Sélectionnez **Modifier la fiche**. 2. Saisissez les **Préférences**, les **Contre-indications** et la **Note**. 3. Sélectionnez **Enregistrer la fiche**. <figure> <img src="/images/guide/fr/staff/customer-sheet-edit.png" alt="Fiche client en cours de modification avec une préférence d’exemple, une allergie aux fruits à coque d’exemple dans Contre-indications, une Note vide et les boutons Enregistrer la fiche et Annuler, Enregistrer la fiche entouré." width="645" height="452" /> <figcaption> L’enregistrement d’une allergie d’exemple sur la fiche de Zoé Lefèvre. </figcaption> </figure> La fiche est la même sur le profil et sur chacune des réservations de la personne dans cette catégorie. Les membres de l’équipe qui ne sont pas administrateurs peuvent la modifier quand ils sont affectés à la ressource de l’une des réservations de la personne, jusqu’à un jour après la fin de cette réservation. # Proposer un horaire pour une demande Répondez à la demande de cours particulier d’un membre en lui proposant un horaire. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/reservation-requests Dans certaines catégories, comme les cours particuliers, les membres envoient une demande et le club propose l’horaire. Cette page montre comment trouver ces demandes, proposer un horaire, puis modifier ou retirer votre proposition. ## 1. Trouver les demandes [#1-trouver-les-demandes] Les demandes apparaissent au-dessus de l’agenda de leur catégorie dans **Réservations**, sous **Demandes en attente d’horaire**. Sélectionnez **Ouvrir** pour en lire une. Les coachs les retrouvent aussi dans **Mes réservations** et dans **Agenda du club**. Voir [Vos réservations et disponibilités](/fr/guide/staff/schedule-and-availability). Une demande affiche **En attente** et une carte **Demande de réservation** sur sa page. Les membres peuvent vous écrire dans la **Conversation** en dessous. Voir [Répondre aux messages](/fr/guide/staff/chats). <figure> <img src="/images/guide/fr/staff/reservation-request.png" alt="Page de la demande Yoga with Thomas d’Alex Morgan, marquée En attente, avec une carte Demande de réservation contenant un bouton Proposer un horaire, et le début d’une conversation où Alex demande un cours de yoga en soirée en semaine." width="1280" height="800" /> <figcaption> La demande d’exemple Yoga with Thomas, vue par le compte de coach Sport coach. La commande à utiliser est entourée en orange. </figcaption> </figure> ## 2. Proposer un horaire [#2-proposer-un-horaire] 1. Sélectionnez **Proposer un horaire**. 2. Saisissez la **Date de début**, l’**Heure de début**, la **Date de fin** et l’**Heure de fin**. Quand vous changez le début, la fin suit pour garder la même durée. 3. Activez **Offrir cette séance** pour ne pas faire payer la séance. L’option apparaît quand la séance a un prix. 4. Sélectionnez **Envoyer la proposition**. Le membre voit l’horaire proposé sur sa réservation et peut l’accepter. <figure> <img src="/images/guide/fr/staff/propose-a-time.png" alt="Fenêtre Proposer un horaire avec une date de début et de fin, une heure de début à 18:00, une heure de fin à 19:00, Offrir cette séance activé et un bouton Envoyer la proposition." width="448" height="348" /> <figcaption> Proposition d’une séance de yoga gratuite d’une heure à 18:00. </figcaption> </figure> ## 3. Modifier ou retirer une proposition [#3-modifier-ou-retirer-une-proposition] Une fois la proposition envoyée, la carte affiche l’horaire proposé et le **Total**, marqué **Séance gratuite** si vous avez offert la séance. **Confirmée dès acceptation.** signifie que la réservation est confirmée dès que le membre accepte. **Paiement dû après acceptation.** signifie que le membre paie après avoir accepté. <figure> <img src="/images/guide/fr/staff/reservation-proposal.png" alt="Carte Demande de réservation avec un horaire proposé de 18:00 à 19:00, la mention Séance gratuite, un total de 0 Stars, la ligne Confirmée dès acceptation et un bouton Modifier la proposition." width="1044" height="176" /> <figcaption> La proposition envoyée pour la demande d’exemple Tennis with Julien. </figcaption> </figure> * **Modifier la proposition** envoie un nouvel horaire à la place du précédent. * **Retirer la proposition** apparaît quand vous proposez un nouvel horaire pour une réservation qui en a déjà un. Ce bouton annule la proposition, et la réservation garde son horaire actuel. Tant qu’une demande attend un horaire, **Annuler** est le seul bouton de statut. Dans ces catégories, **Modifier** n’est disponible que tant que la demande est **En attente**. # Gérer les réservations Consultez, trouvez, créez, déplacez et annulez les réservations dans l’agenda. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/reservations La section **Réservations** affiche toutes les réservations de votre club dans un agenda, une [catégorie de réservation](/fr/guide#reservation-category) à la fois. Une catégorie de réservation regroupe ce que les membres peuvent réserver, comme un restaurant, une salle de réunion ou des cours collectifs. Chaque élément réservable d’une catégorie, comme une table ou une salle, s’appelle une ressource. Cette page montre comment lire l’agenda, trouver une réservation, en ajouter une, changer son horaire et l’annuler. D’autres tâches sur les réservations ont leurs propres pages : * [Personnes, produits et fiches clients d’une réservation](/fr/guide/staff/reservation-people) : inviter quelqu’un, marquer la présence, ajouter des produits et tenir une fiche client. * [Proposer un horaire pour une demande](/fr/guide/staff/reservation-requests) : répondre aux demandes des catégories comme les cours particuliers. * [Périodes bloquées, séances et listes d’attente](/fr/guide/staff/blocked-periods-and-waitlists) : fermer temporairement une ressource et voir qui attend une place. ## 1. Choisir une catégorie et une période [#1-choisir-une-catégorie-et-une-période] Ouvrez **Réservations** dans le menu de gauche. La barre du haut liste les catégories de votre club. Sélectionnez-en une pour afficher son agenda. Quand la liste est longue, utilisez les flèches à chaque bout de la barre, ou sélectionnez la loupe et saisissez une partie du nom d’une catégorie. <figure> <img src="/images/guide/fr/staff/reservations-calendar.png" alt="Agenda des réservations de la catégorie Salle de réunion en vue Semaine : la ligne Conference room affiche une réservation par jour à 13:00 et une période bloquée le lundi." width="1280" height="800" /> <figcaption> Une semaine de la salle de réunion d’exemple Salle de réunion à Ukab Clubs. La commande à utiliser est entourée en orange, et les numéros renvoient à la liste ci-dessous. </figcaption> </figure> 1. Chaque ligne est une ressource, ici la **Conference room**. Chaque bloc coloré est une réservation avec son heure de début et le nom du client. Un bloc rouge marqué **Bloqué** est une période pendant laquelle personne ne peut réserver. 2. **Semaine** et **Jour** permettent de passer d’une semaine entière à une seule journée avec les heures. En **Semaine**, chaque jour affiche ses deux premières réservations sous forme de cartes, et un lien comme **+1 réservation** ouvre la journée quand elle en a davantage. Les réservations sur plusieurs jours, comme les séjours à l’hôtel, s’étendent sur toute leur durée. Les catégories avec un plan des espaces affichent la semaine sous forme de frise. Sélectionnez le nom d’un jour dans l’en-tête pour ouvrir directement cette journée. 3. Les flèches de chaque côté des dates passent à la période précédente ou suivante. Sélectionnez les dates pour choisir un jour dans un calendrier, et l’icône de calendrier pour revenir à aujourd’hui. 4. Les boutons de droite dépendent de la catégorie et de votre [rôle](/fr/guide#role). **Paramètres de la catégorie** modifie le fonctionnement de la catégorie ; c’est votre administrateur qui le règle. Voir [Configurer les catégories de réservation](/fr/guide/admins/reservation-categories). **Périodes bloquées**, **Gérer les séances** et **Liste d’attente** sont expliqués dans [Périodes bloquées, séances et listes d’attente](/fr/guide/staff/blocked-periods-and-waitlists). **Billets** contrôle les billets à QR code des membres à l’entrée. Voir [Enregistrer l’arrivée et le départ des clients](/fr/guide/staff/check-in-and-deposits). > Pour les cours particuliers, un membre peut demander une séance sans choisir d’horaire. Ces > demandes apparaissent au-dessus de l’agenda sous **Demandes en attente d’horaire**. Sélectionnez > **Ouvrir** pour lire la demande et proposer un horaire. Voir [Proposer un horaire pour une > demande](/fr/guide/staff/reservation-requests). ## 2. Trouver une réservation [#2-trouver-une-réservation] Saisissez un nom dans **Rechercher...** au-dessus de l’agenda. La liste sous le champ affiche chaque réservation correspondante avec sa date et son heure. Sélectionnez-en une pour l’ouvrir. <figure> <img src="/images/guide/fr/staff/reservations-search.png" alt="Recherche dans l’agenda avec un prénom saisi et la réservation correspondante de la Conference room avec sa date et son heure, suivie d’un bouton Ajouter une réservation." width="1280" height="800" /> <figcaption> La recherche du prénom d’un membre dans l’agenda d’exemple Salle de réunion. </figcaption> </figure> ## 3. Ajouter une réservation [#3-ajouter-une-réservation] Survolez un créneau libre de l’agenda et sélectionnez le **+** qui apparaît. Le formulaire **Ajouter une réservation** s’ouvre avec la ressource et l’horaire choisis. Vous pouvez aussi sélectionner **Ajouter une réservation** en bas de la liste de recherche : le formulaire s’ouvre alors sur le jour affiché à 9:00, ou dans une minute s’il s’agit d’aujourd’hui et que 9:00 est passé. Vérifiez donc la ressource et l’horaire. Ajouter une réservation nécessite l’autorisation **Créer et modifier les réservations**. <figure> <img src="/images/guide/fr/staff/reservation-form.png" alt="Formulaire Ajouter une réservation avec Réservation sélectionné, un champ Client vide, la ressource Conference room, 1 personne, les dates et heures de début et de fin, et les boutons Fermer et Enregistrer la réservation, Enregistrer la réservation entouré en orange." width="512" height="528" /> <figcaption> Le formulaire d’une réservation de la Conference room d’exemple, de 16:00 à 17:00. </figcaption> </figure> 1. Dans **Client**, commencez à taper le nom d’un membre et choisissez-le dans la liste, ou saisissez le nom d’un invité qui n’est pas membre. 2. Vérifiez la **Ressource** et le **Nombre de personnes**. 3. Ajustez la **Date de début**, l’**Heure de début**, la **Date de fin** et l’**Heure de fin**. 4. Sélectionnez **Enregistrer la réservation**. La réservation apparaît dans l’agenda. Une réservation avec un prix ou une caution reste **Réservée · paiement dû** jusqu’à ce que le client paie depuis son compte. Champagne ne peut pas enregistrer un paiement reçu à l’accueil. Quand la ressource propose des produits, comme des boissons, le formulaire les liste aussi sous **Produits**. Voir [Ajouter des produits à une réservation](/fr/guide/staff/reservation-people#3-ajouter-des-produits-%C3%A0-une-r%C3%A9servation). L’onglet **Période bloquée** en haut du formulaire ferme temporairement une ressource. Voir [Bloquer une période](/fr/guide/staff/blocked-periods-and-waitlists#1-bloquer-une-p%C3%A9riode). ## 4. Ouvrir, déplacer ou annuler une réservation [#4-ouvrir-déplacer-ou-annuler-une-réservation] Sélectionnez une réservation dans l’agenda pour ouvrir sa page. Elle affiche le statut, le début et la fin, l’origine de la réservation, les personnes inscrites, leur présence et l’historique des changements. **Retour à** suivi du nom de la catégorie ramène à l’agenda. Quand la réservation appartient à une personne de **Personnes**, **Ouvrir la fiche** ouvre sa page ; **Retour à Réservation** vous y ramène. <figure> <img src="/images/guide/fr/staff/reservation-detail.png" alt="Page de réservation de Sam Rivera, Confirmée, dans la Conference room, avec les boutons Modifier et Annuler, les heures de début et de fin, les personnes sur la réservation, la présence et l’historique." width="1280" height="800" /> <figcaption> La réservation du membre d’exemple Sam Rivera dans la Conference room. </figcaption> </figure> **Modifier** et **Annuler** nécessitent l’autorisation **Créer et modifier les réservations**. Quand votre rôle n’a pas cette autorisation, les boutons n’apparaissent pas. Pour déplacer une réservation, sélectionnez **Modifier**. Changez la **Ressource**, le **Nombre de personnes** ou les dates et heures, puis sélectionnez **Enregistrer la réservation**. Si le nouvel horaire ou le nouvel endroit change le prix d’une réservation confirmée, la différence est prélevée sur le solde de [crédits](/fr/guide#credits) du client ou lui est rendue, même s’il a payé par carte. Quand son solde ne suffit pas pour un prix plus élevé, la modification est refusée et il doit d’abord recharger. Une réservation faite pour un nom saisi, ou qui attend encore son paiement, change seulement de prix. Pour annuler une réservation, sélectionnez **Annuler**. Une fenêtre résume la réservation. Sélectionnez **Annuler la réservation** pour confirmer, ou **Conserver la réservation** pour la laisser telle quelle. > Quand une réservation payée est annulée, le client est remboursé automatiquement. Si le délai > d’annulation est dépassé, la fenêtre propose **Rembourser le prix** suivi du prix : laissez la > case décochée pour garder le prix et ne rendre que la caution. Les boutons en haut de la page dépendent du statut. | Statut | Ce que vous pouvez faire | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **En attente** | **Confirmer** accepte la réservation. **Réserver** apparaît à la place quand le client doit encore payer. **Annuler** la refuse. | | **Réservée · paiement dû** | **Annuler**. La réservation devient **Confirmée** une fois que le client a payé. | | **Confirmée** | **Annuler**. | | **Terminé** | Rien. La réservation ne peut plus être modifiée. | | **Annulée** | Rien. La réservation ne peut plus être modifiée. | Chaque bouton ouvre une fenêtre qui résume la réservation et vous demande de confirmer. Le reste de la page de réservation liste les personnes inscrites sur la réservation, leur présence et les produits facturés. Voir [Personnes, produits et fiches clients d’une réservation](/fr/guide/staff/reservation-people). # Finances Retrouvez une facture, vérifiez ce qui a été payé et émettez un avoir. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/sales-and-invoices La section **Finances** liste les factures de votre club : adhésions, abonnements et frais ponctuels. Cette page montre comment retrouver une facture, la lire et émettre un avoir quand un membre doit payer moins. La section apparaît quand votre [rôle](/fr/guide#role) comprend l’autorisation **Gérer les factures et les avoirs**. ## 1. Retrouver une facture [#1-retrouver-une-facture] Dans le menu de gauche, sélectionnez **Finances**. La liste affiche d’abord les factures les plus récentes, avec leur numéro de **Facture**, le **Client**, la date **Émise**, l’**Échéance**, le **Total** et le **Statut**. <figure> <img src="/images/guide/fr/staff/invoices.png" alt="Section Finances listant quatre factures pour Sam Rivera, Émilie Roche, Hugo Lemaire et Jamie Chen, avec leurs totaux et les statuts En retard, Payée et Annulée." width="1280" height="800" /> <figcaption> Des factures d’exemple d’Ukab Clubs, trouvées en recherchant example.com. </figcaption> </figure> * Saisissez dans le champ de recherche pour trouver une facture par numéro, client ou e-mail. * Sélectionnez **Filtres** pour n’afficher qu’un **Statut**, ou les factures émises entre une date **Du** et une date **Au**. Sélectionnez **Appliquer les filtres** pour mettre la liste à jour, et **Effacer les filtres** pour tout revoir. * Sélectionnez le titre d’une colonne pour trier la liste selon cette colonne. | Statut | Ce qu’il signifie | | ----------------- | ---------------------------------------------------------- | | **À payer** | La facture attend son paiement. | | **Payée** | La facture est entièrement payée. | | **En retard** | L’échéance est passée et la facture reste à payer. | | **Brouillon** | La facture est en préparation et n’a pas encore de numéro. | | **Annulée** | La facture a été annulée et n’a plus à être payée. | | **Irrécouvrable** | Le club ne compte plus être payé pour cette facture. | Quand une partie d’une facture a été créditée, le montant suivi de **crédité** apparaît sous son total, par exemple 1 800,00 € crédité. ### Quand la liste est vide [#quand-la-liste-est-vide] | Message | Ce qu’il signifie | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **La facturation n’est pas encore connectée** | Votre club n’a pas encore de compte de facturation. Si vous pouvez ouvrir **Paramètres**, sélectionnez **Connecter la facturation**. Sinon, demandez à un admin du club. | | **Aucune facture trouvée** | Rien ne correspond à votre recherche ou à vos filtres : sélectionnez **Effacer la recherche et les filtres**. Sans recherche ni filtre, aucun abonnement n’a encore été facturé. | | **Aucune facture locale à afficher pour le moment** | Champagne copie encore les factures de votre compte de facturation. D’autres apparaissent au fur et à mesure. | Champagne garde une copie des factures de votre compte de facturation. Pendant la mise à jour de cette copie, la liste affiche **Synchronisation de la facture en cours**. Si elle affiche **Échec de la synchronisation de la facture**, la liste n’est peut-être pas à jour. Prévenez votre fournisseur Champagne si le message persiste. Voir [Paiements](/fr/guide/admins/payments) pour connecter un compte de facturation. ## 2. Lire une facture [#2-lire-une-facture] Sélectionnez une facture pour l’ouvrir. <figure> <img src="/images/guide/fr/staff/invoice.png" alt="Facture payée de Jamie Chen montrant le client facturé, la ligne d’adhésion Adhésion 51 IÉNA, un paiement reçu, le total de la facture, une carte Ajuster cette facture et un bouton Télécharger le PDF entouré en orange." width="1280" height="800" /> <figcaption> Une facture d’adhésion d’exemple payée par Jamie Chen. La commande à utiliser est entourée en orange. </figcaption> </figure> * **Facturé à** affiche le client et son e-mail. **Profil associé** affiche le membre concerné ; sélectionnez **Ouvrir la fiche** en haut pour ouvrir sa page dans **Personnes**. * En dessous, la facture indique s’il s’agit d’une **Facture d’abonnement** ou d’une **Facture ponctuelle**, si une adhésion est enregistrée sur le profil du membre, et le nom de l’émetteur sous **Émis par**. * Les lignes de la facture listent chaque article avec sa quantité et son montant. Une ligne d’adhésion affiche aussi son prix et sa période. Une ligne **Ajustement au prorata pour …** corrige le prix quand un [plan](/fr/guide#plan) a changé pendant une période de facturation. * **Paiements** liste chaque paiement avec son état : **Paiement reçu** avec sa date et son montant, ou **Paiement en attente** et **Paiement annulé** avec le montant demandé, en gris. * **Avoirs** liste les avoirs déjà émis pour cette facture. * **Total de la facture** affiche le **Sous-total**, le **Total hors taxes (HT)**, la taxe (**TVA**), le **Total toutes taxes comprises (TTC)**, les lignes **Payée** et **Crédité**, et, tant que la facture reste due, le montant **Restant** et la date **À payer**. Sélectionnez **Télécharger le PDF** pour enregistrer une copie de la facture. Quand la facture reste à payer, le bouton **Lien de paiement** ouvre sa page de paiement en ligne dans un nouvel onglet. ## 3. Émettre un avoir [#3-émettre-un-avoir] Un avoir est un document qui annule tout ou partie d’une facture. Utilisez-le pour corriger une erreur ou pour faire un geste commercial. 1. Ouvrez la facture et sélectionnez **Créer un avoir** dans la carte **Ajuster cette facture**. 2. Vérifiez le montant dans la question, par exemple **Créditer 1 800,00 € sur** suivi du numéro de la facture. 3. Saisissez un **Motif**, jusqu’à 500 caractères. Il est obligatoire et conservé avec l’avoir. 4. Sélectionnez **Confirmer l’avoir**. <figure> <img src="/images/guide/fr/staff/credit-note.png" alt="Fenêtre Créer un avoir qui demande de créditer 1 800,00 € sur la facture, avec un motif d’exemple et les boutons Annuler et Confirmer l’avoir." width="1280" height="800" /> <figcaption> Un avoir en préparation sur la facture d’exemple de Jamie Chen. </figcaption> </figure> L’avoir couvre tout le montant qui peut encore être crédité. Il réduit ce que le client doit. Toute somme déjà payée est reportée sur son solde et servira à régler ses prochaines factures. > Émettre un avoir ne renvoie pas d’argent sur la carte du client et n’annule pas son adhésion. Une fois émis, l’avoir apparaît dans **Avoirs** avec son numéro, sa date, son motif et son montant. Sélectionnez **Télécharger l’avoir** pour l’enregistrer en PDF. Un avoir annulé affiche **· Annulée** après son numéro. <figure> <img src="/images/guide/fr/staff/credit-notes.png" alt="Carte Avoirs listant l’avoir EFAVXZ3T-0006-CN-01 de 450,00 € avec sa date, le motif Avoir partiel de démonstration et un lien Télécharger l’avoir." width="645" height="136" /> <figcaption> Un avoir partiel d’exemple sur la facture d’adhésion d’Émilie Roche. </figcaption> </figure> Quand toute la facture est créditée, la carte **Ajuster cette facture** n’apparaît plus. # Vos réservations et disponibilités Consultez les réservations prises avec vous et indiquez les heures où vous pouvez travailler. Source: https://docs.champagne.dev-ukab.fr/fr/guide/staff/schedule-and-availability Les coachs et les autres membres de l’équipe qui prennent des réservations à leur nom ont deux sections personnelles. **Mes réservations** liste les réservations prises avec vous. **Mes disponibilités** contient les heures où vous pouvez travailler : les membres ne peuvent vous réserver que pendant ces heures. ## 1. Consulter vos réservations [#1-consulter-vos-réservations] Ouvrez **Mes réservations** dans le menu de gauche. Vos prochaines réservations sont regroupées par jour, en commençant par **Aujourd’hui** et **Demain**. Chaque ligne affiche l’horaire, le nom du client, ce qu’il a réservé et le statut de la réservation. <figure> <img src="/images/guide/fr/staff/my-schedule.png" alt="Page Mes réservations avec une carte Demandes en attente d’horaire listant deux demandes d’Alex Morgan, puis les réservations du jour avec leurs horaires, les noms des clients, Tennis with Julien ou Yoga with Thomas, et le statut Réservée · paiement dû." width="1280" height="800" /> <figcaption> Les réservations du compte de coach d’exemple Sport coach chez Ukab Clubs. </figcaption> </figure> | Statut | Ce qu’il signifie | | -------------------------- | ---------------------------------------------------- | | **En attente** | La réservation attend la décision du club. | | **Réservée · paiement dû** | La place est retenue et le client doit encore payer. | | **Confirmée** | La réservation est confirmée. | | **Terminé** | La réservation est passée. | | **Annulée** | La réservation a été annulée. | Sélectionnez une ligne pour ouvrir la réservation. Sélectionnez **Afficher plus** en bas pour voir les réservations plus lointaines. > Quand un membre demande une séance sans choisir d’horaire, la demande apparaît en haut, sous > **Demandes en attente d’horaire**. **En attente de votre proposition** signifie que vous devez > proposer un horaire : ouvrez la demande et sélectionnez **Proposer un horaire**. **Proposition > envoyée** signifie que le membre a reçu votre proposition et n’a pas encore répondu. Voir > [Proposer un horaire pour une demande](/fr/guide/staff/reservation-requests). ## 2. Voir vos heures de la semaine [#2-voir-vos-heures-de-la-semaine] Ouvrez **Mes disponibilités**. La grille montre une semaine, avec vos heures de travail en vert. Un bloc marqué **Absent** est une absence à cette date. Utilisez les flèches à côté des dates pour changer de semaine, et l’icône de calendrier pour revenir à la semaine en cours. <figure> <img src="/images/guide/fr/staff/my-availability.png" alt="Page Mes disponibilités pour une semaine, avec des blocs verts de 08:00 à 12:00 et de 14:00 à 21:00 du lundi au samedi et de 10:00 à 12:00 le dimanche, et un bouton Ajouter des heures." width="1280" height="800" /> <figcaption> Une semaine d’heures de travail du compte de coach d’exemple Sport coach. </figcaption> </figure> ## 3. Ajouter ou retirer des heures [#3-ajouter-ou-retirer-des-heures] Pour ajouter des heures, sélectionnez **Ajouter des heures**, ou faites glisser la souris vers le bas sur une partie vide d’un jour dans la grille. Saisissez la **Date**, l’**Heure de début** et l’**Heure de fin**, puis choisissez la portée du changement : * **Seulement le** suivi de la date change ce seul jour. * **Chaque** suivi du jour de la semaine change ce jour toutes les semaines. <figure> <img src="/images/guide/fr/staff/availability-add-hours.png" alt="Fenêtre Ajouter des heures avec une date, une heure de début à 12:00, une heure de fin à 14:00, et les boutons Annuler, Seulement le suivi de la date et Chaque suivi du jour de la semaine, ces deux derniers entourés." width="512" height="292" /> <figcaption> Ajout des heures d’exemple de 12:00 à 14:00 le jour même. Les commandes à utiliser sont entourées en orange. </figcaption> </figure> Pour retirer des heures, sélectionnez un bloc vert. La fenêtre **Retirer des heures** s’ouvre avec ses horaires déjà remplis. Raccourcissez les horaires pour ne retirer qu’une partie du bloc, puis choisissez **Seulement le** suivi de cette date ou, pour des heures hebdomadaires, **Chaque** suivi de ce jour de la semaine. Retirer des heures pour une seule date ajoute une absence ce jour-là. Pour annuler une absence, sélectionnez le bloc **Absent** et validez la fenêtre **Supprimer cette absence ?**. Vos heures habituelles s’appliquent de nouveau à cette date. > Le club refuse une réservation avec vous en dehors de vos heures de disponibilité, et indique que > vous n’êtes pas disponible à ce moment-là.