Skip to main content

Support, Referral, Merchant & Host Profiles

What it is​

Four smaller, independent surfaces bundled here: support (in-app help requests), referral (campaign-based referral rewards), merchant (a large module — merchant applications plus a full merchant storefront/wallet, structurally parallel to Vendor/VendorProfile), and host-profiles (a user's public-facing "host" identity, separate from their account profile).

How it works​

Support​

Minimal: GET /support/categories (what a request can be about), POST /support/request (create one, authenticated), POST /support/upload-image (attach a screenshot). No visible ticket-status tracking endpoints in this module — likely handled by a downstream support tool once the request is created.

Referral​

Rewards are tied to a current campaign, not evergreen — GET /referral/campaign returns whatever gifting campaign is currently active. Users see GET /referral/pending / GET /referral/my-rewards / GET /referral/summary, and cash out via POST /referral/claim-rewards (transaction-PIN gated, the same step-up pattern seen in wallets elsewhere). Admin has a parallel view (GET /referral/pending-rewards, PUT /referral/rewards/:rewardId/mark-paid) — rewards apparently need an admin-side "mark paid" step even after a user claims, suggesting claim and payout settlement are two separate steps, not one atomic action.

Merchant​

Structurally similar to Vendors but a separate, larger module (~40 routes) with its own wallet (POST /merchant/wallet, funding account, pocket transfers — the same PiggyVest pattern as the User and PartyBank wallets). Becoming a merchant starts with an interest application, not direct signup: GET /merchant/interest/options / .../form-fields (discover what application types exist and what the form needs), POST /merchant/interest (submit), GET /merchant/interest/my-submissions (track your own applications). Once approved (elsewhere), a merchant manages an "active profile" (GET/PUT /merchant/active), gift-store products, and orders through the rest of the module.

applicationType is one of vendor, merchant, eventSponsor, or ticketHero — these are genuinely different partnership types sharing one application table (merchant_interests), not sub-categories of "merchant." A (userId, applicationType) unique DB constraint means a user can hold one open application per type simultaneously (e.g. apply as both vendor and eventSponsor at once) but can't double-submit the same type — unless their prior application for that type was rejected, which doesn't count against the constraint and allows re-applying.

Host Profiles​

A user can have multiple host profiles (not one-per-account) with one marked default (PATCH /host-profiles/:id/set-default, DELETE /host-profiles/default) — created via multipart form (name, description, website, socialMedia, address as JSON, optional avatar file upload). Public read: GET /host-profiles/events/:eventId/hosts/public (no auth — shows only approved, active host profiles with their role on the event: main_host, co_host, organizer, speaker, or performer, ordered by displayOrder). Like vendor profiles, host profiles go through admin approval: GET /admin/host-profiles/pending, PATCH /admin/host-profiles/:id/approve — a profile is unusable on any event until approved.

Two profile types exist: custom (any organization/brand a user sets up) and user_profile (a special profile that mirrors the user's own account identity) — a (userId, type='user_profile') unique constraint means each user gets at most one self-mirroring profile, but unlimited custom ones. An event can have multiple hosts attached via the event_hosts junction table, each with an independent role and displayOrder for how they're listed.

Multipart + Zod don't mix here — this module's create/update endpoints intentionally use the legacy validate() (validatorjs) middleware, not validateZod(), because Zod can't parse the structured-but-stringified fields (socialMedia, address as JSON strings) that arrive over multipart/form-data the way the traditional validator can. If you're adding a new file-upload endpoint elsewhere, this is a real, previously-hit gotcha — don't assume validateZod() + multipathMiddleware composes cleanly.

Key business rules & edge cases​

  • Referral reward "claim" and "mark paid" are separate steps (user-initiated claim, admin-confirmed payout) — don't treat a successful claim-rewards call as proof money has actually moved; check the admin-side status too if building a reconciliation view.
  • Merchant onboarding is gated by an approval workflow behind the interest-application submission — don't assume POST /merchant/interest grants merchant access; it's a request, not a signup.
  • A user can hold multiple host profiles; code that assumes "the user's host profile" (singular) should use GET /host-profiles/default or .../user-profile, not just grab the first result from a list.
  • A rejected merchant interest application doesn't block a resubmission for the same applicationType, but pending/reviewed/approved ones do — the uniqueness check explicitly excludes status = 'rejected'.

Not yet in the generated OpenAPI spec. POST /v1/support/request, GET /v1/referral/summary, POST /v1/referral/claim-rewards, POST /v1/merchant/interest, GET /v1/host-profiles, PATCH /v1/host-profiles/{id}/set-default, GET /v1/user/events/{eventId}/hosts/public.

  • src/modules/support/ — routes/controller
  • src/modules/referral/referral.routes.ts, referral.controller.ts, referral.service.ts
  • src/modules/merchant/merchant.routes.ts, merchant.controller.ts, merchant.service.ts
  • src/models/merchantInterest.model.ts — ApplicationType enum
  • src/modules/host-profiles/ — routes/controller; validation in src/shared/validations/host-profile.validation.ts