Skip to main content

Loyalty & Offers

Two related areas: the gems loyalty programme (a customer’s own balance, transaction history, and the redeemable-services catalogue) and offers (a public list of active promotions plus manager-only CRUD).
Base URLs: the customer endpoints (GET /api/gems, public GET /api/offers) are served from https://theroyalglow.in. The admin offer management endpoints are served from https://admin.theroyalglow.in with no /api/admin/ segment — the subdomain is the namespace (e.g. admin.theroyalglow.in/api/offers). Note /api/offers therefore exists in both apps: a public read on theroyalglow.in and role-gated CRUD on admin.theroyalglow.in. Money is an integer in paise (₹1,000.00 = 100000). Calendar dates are YYYY-MM-DD on input; date-mode columns serialise back as UTC-midnight ISO strings (e.g. "2026-06-30T00:00:00.000Z"). The role hierarchy is customer < staff < receptionist < manager < owner < developer.
Gems rules. Customers earn 1 gem per ₹100 invoiced (floor), on service invoices only — never on membership purchases or sessions. Gems expire 365 days after they are earned. Redemption is against specific catalogue services, not a rupee discount, and gems cannot be combined with an offer on the same booking.

GET theroyalglow.in/api/gems

Returns the signed-in customer’s own loyalty summary, a paginated transaction history, and the redeemable-services catalogue. Strictly scoped to session.user.id — it never exposes another customer’s data. A loyalty account is created on first call, so a brand-new customer sees zeros rather than an error. Minimum role: customer (requireSession)

Query parameters

Response

data.summary is the balance plus lifetime totals (zeros if the account was just created). data.transactions are newest-first; invoiceNumber is null for non-invoice transactions (e.g. expired/adjusted). data.redeemable is the active gems-redeemable services catalogue. meta carries page and totalPages — there is no separate count query, so totalPages is page + 1 whenever a full page is returned (there may be more), otherwise page.
loyalty_tx_type is one of earned, redeemed, expired, adjusted.

Errors


GET theroyalglow.in/api/offers

Public list of active offers for the customer offers page. No auth. Returns only offers whose isActive flag is true and whose calendar date range includes today, ordered by displayOrder. Each offer carries its linked services. Minimum role: Public (no session required)

Response

Each offer is the full offer row plus services ({ id, name }[]) and the convenience arrays serviceIds and serviceNames. The discount field that is populated depends on offerType:
  • percentagediscountPercentage (1–100)
  • flatdiscountAmountPaise (paise)
  • combo_pricecomboPricePaise (paise)

Errors


GET admin.theroyalglow.in/api/offers

Lists all offers (admin view), newest first, each with its linked services. Minimum role: manager (requireRole('manager'))

Response

Same per-offer shape as GET /api/offers, but unfiltered (includes inactive and out-of-range offers), ordered by createdAt descending.

Errors


POST admin.theroyalglow.in/api/offers

Creates an offer and its service links. The slug is derived server-side from the name (lowercased, hyphenated, with a short nanoid suffix so duplicate names never collide). The calendar dates are converted to UTC-midnight Dates. Minimum role: manager (requireRole('manager'))

Request body

Validated by createOfferSchema (@rgss/types). The discount field that matches offerType is required, and endDate must be on or after startDate (both enforced via superRefine, surfaced as field-level errors).
Requires discountPercentage.

Response

Returns 201 Created with the created offer (the raw offer row).

Errors


GET admin.theroyalglow.in/api/offers/[id]

Returns a single offer with its linked services. Minimum role: manager (requireRole('manager'))

Path parameters

Response

Same per-offer shape as the list endpoints (offer row plus services, serviceIds, serviceNames).

Errors


PATCH admin.theroyalglow.in/api/offers/[id]

Updates offer fields or toggles isActive. A bare { "isActive": false } (with no other fields) deactivates via the dedicated query; any other combination maps the provided fields (dates converted to Date) and updates. When serviceIds is supplied, the offer’s service set is fully replaced. Minimum role: manager (requireRole('manager'))

Path parameters

Request body

Validated by updateOfferSchema — every field from createOfferSchema is optional, plus an optional isActive boolean. The conditional discount/date refinements that apply on create are not re-run here (the base object is made partial).
isActive: false alone deactivates the offer.

Response

Returns the updated offer row.

Errors

Last modified on June 29, 2026