Champagne

Club credits

Credit settings, one-time refills, wallets and purchases.

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.

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

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

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.

mutation {
  configureCredits(
    clubId: "51000000-0000-4000-8000-000000000051"
    creditName: "Stars"
    creditsPerEuro: 1000
    lowBalanceThreshold: 1000
  ) {
    creditName
    creditsPerEuro
    lowBalanceThreshold
  }
}

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.

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

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

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.

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

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 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); 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 <resource> · Reservation change (or · Products when only products changed); see 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 pay their booking by card. Existing cash-payment webhook and refund processing remains available for outstanding payments.

Sur cette page