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 plusservices ({ id, name }[]) and the
convenience arrays serviceIds and serviceNames. The discount field that is
populated depends on offerType:
percentage→discountPercentage(1–100)flat→discountAmountPaise(paise)combo_price→comboPricePaise(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 asGET /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 bycreateOfferSchema (@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).
- Tab Title
- Tab Title
- Tab Title
Requires
discountPercentage.Response
Returns201 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 plusservices,
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 byupdateOfferSchema — 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).
- Tab Title
- Tab Title
isActive: false alone deactivates the offer.