openapi: 3.0.0
info:
  title: OSU Campaign Platform API
  version: 1.0.0
  description: API for managing campaign collections, contributions, and Threads integration
servers:
  - url: /api
paths:
  /admin/kol-referrals:
    get:
      summary: Admin - Find KOL referral account by exact email
      operationId: adminGetKolReferral
      parameters:
        - name: email
          in: query
          required: true
          schema: { type: string, format: email }
      responses:
        '200': { description: KOL referral account, content: { application/json: { schema: { $ref: '#/components/schemas/KolReferralAdminApiResponse' } } } }
        '400': { description: Invalid request }
        '403': { description: Admin required }
        '404': { description: User not found }
    patch:
      summary: Admin - Grant or revoke KOL and configure commission
      operationId: adminUpdateKolReferral
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/AdminKolReferralPatchBody' } } }
      responses:
        '200': { description: Updated KOL referral account, content: { application/json: { schema: { $ref: '#/components/schemas/AdminKolReferralUpdateApiResponse' } } } }
        '400': { description: Invalid request }
        '403': { description: Admin required }
        '404': { description: User not found }
    post:
      summary: Admin - Record an externally paid KOL referral commission
      operationId: adminRecordKolReferralPayout
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/AdminKolReferralPayoutBody' } } }
      responses:
        '200': { description: Payout receipt recorded or returned idempotently, content: { application/json: { schema: { $ref: '#/components/schemas/AdminKolReferralPayoutReceiptApiResponse' } } } }
        '400': { description: Invalid request }
        '403': { description: Admin required }
        '404': { description: User not found }
        '409': { description: "Amount outside unpaid commission bounds, duplicate cutoff, or request conflict" }
        '500': { description: Payout recording failure }
  /kol/referrals:
    get:
      summary: Get current KOL referral dashboard
      operationId: getKolReferrals
      responses:
        '200': { description: KOL referral dashboard, content: { application/json: { schema: { $ref: '#/components/schemas/KolReferralDashboardApiResponse' } } } }
        '401': { description: Authentication required }
        '403': { description: Active KOL required }
    post:
      summary: Create immutable KOL referral code
      operationId: createKolReferralCode
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/KolReferralCodeBody' } } }
      responses:
        '200': { description: Referral code created, content: { application/json: { schema: { $ref: '#/components/schemas/KolReferralCodeApiResponse' } } } }
        '400': { description: Invalid request }
        '401': { description: Authentication required }
        '403': { description: Active KOL required }
        '409': { description: Code already exists }
    patch:
      summary: Enable or disable current KOL referral code
      operationId: updateKolReferralCode
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/KolReferralCodeToggleBody' } } }
      responses:
        '200': { description: Referral code updated, content: { application/json: { schema: { $ref: '#/components/schemas/KolReferralCodeApiResponse' } } } }
        '400': { description: Create a code first }
        '401': { description: Authentication required }
        '403': { description: Active KOL required }
  /threads/insights:
    get:
      summary: Get Post Insights
      operationId: getThreadsPostInsights
      parameters:
        - name: postId
          in: query
          required: true
          schema:
            type: string
        - name: accessToken
          in: query
          required: true
          schema:
            type: string
        - name: metrics
          in: query
          required: false
          schema:
            type: string
            description: Comma-separated list of metrics
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadsInsightsResponse'
  /threads/posts:
    get:
      summary: Get User Threads
      operationId: getUserThreads
      parameters:
        - name: accessToken
          in: query
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: after
          in: query
          required: false
          schema:
            type: string
        - name: before
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadsPostsResponse'
  /threads/posts/my:
    get:
      summary: Get My Threads Posts
      operationId: getMyThreadsPosts
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyThreadsPostsApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/threads/posts:
    get:
      summary: List Admin Threads Posts
      operationId: adminListThreadsPosts
      parameters:
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: hashtag
          in: query
          required: false
          schema:
            type: string
        - name: search
          in: query
          required: false
          schema:
            type: string
        - name: sentiment
          in: query
          required: false
          schema:
            type: string
            enum: [all, positive, negative]
            default: all
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminThreadsPostListApiResponse'
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /threads/sync:
    post:
      summary: Trigger a manual Threads sync (once per day)
      operationId: syncThreads
      responses:
        '200':
          description: Sync completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadsSyncApiResponse'
        '400':
          description: No Threads account connected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '429':
          description: Rate limited - sync already performed today
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaigns:
    get:
      summary: Get Campaign List
      operationId: getCampaignList
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: >
            Search campaigns by title (case-insensitive). Multiple keywords
            separated by spaces are matched independently with AND logic,
            e.g. "summer music" matches titles containing both "summer" and "music".
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: >
            Comma-separated list of statuses to include.
            Allowed values: DRAFT, ACTIVE, PAUSED, COMPLETED, CANCELLED.
            Defaults to all statuses except DRAFT and CANCELLED.
          example: ACTIVE,PAUSED
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum: [AGENDA]
          description: >
            Optional campaign-list ordering mode. AGENDA ranks ACTIVE before
            COMPLETED, orders ACTIVE by endDate ascending, orders COMPLETED by
            endDate descending, then uses createdAt descending and id ascending
            as deterministic tie-breakers. Omitted sort preserves newest-created
            ordering.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number (1-indexed)
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Number of items per page (max 100)
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignListApiResponse'
        '400':
          description: Invalid query parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaigns/{id}:
    get:
      summary: Get Campaign Detail
      operationId: getCampaignDetail
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Campaign ID
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignDetailApiResponse'
        '400':
          description: Invalid campaign ID format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaigns/{slug}/participation:
    post:
      summary: Submit Threads/vocus URLs and immediately award Campaign participation points
      operationId: submitCampaignParticipation
      parameters:
        - name: slug
          in: path
          description: Campaign slug or UUID
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignParticipationSubmissionRequest'
      responses:
        '200':
          description: OTP requested or authenticated submission completed with participation points awarded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignSubmissionApiResponse'
        '400':
          description: Invalid URL or request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: The authenticated account has not verified its email address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign unavailable or an existing submission is locked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Sign-in code required or authentication missing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Submission failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaign-submissions:
    get:
      summary: List Campaign post submissions
      operationId: listCampaignSubmissions
      description: Admin role required. The operator role cannot access Campaign submission operations.
      parameters:
        - { name: campaignId, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string, enum: [PENDING, APPROVED, REJECTED] } }
        - { name: rewarded, in: query, schema: { type: boolean } }
        - { name: q, in: query, schema: { type: string } }
        - { name: submittedFrom, in: query, schema: { type: string, format: date-time } }
        - { name: submittedTo, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
        - { name: offset, in: query, schema: { type: integer, minimum: 0, default: 0 } }
      responses:
        '200':
          description: Filtered submissions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignSubmissionListApiResponse'
        '403':
          description: Admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaign-submissions/review:
    post:
      summary: Legacy bulk approve or reject Campaign submissions
      deprecated: true
      operationId: reviewCampaignSubmissions
      description: Admin role required. The operator role cannot access Campaign submission operations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignSubmissionReviewRequest'
      responses:
        '200':
          description: Review decisions applied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignSubmissionMutationApiResponse'
        '400':
          description: Invalid request or missing rejection reason
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaign-submissions/award:
    post:
      summary: Legacy batch-award approved Campaign submissions
      deprecated: true
      operationId: awardCampaignSubmissions
      description: Admin role required. The operator role cannot access Campaign submission operations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignSubmissionAwardRequest'
      responses:
        '200':
          description: Idempotent awards applied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignSubmissionAwardApiResponse'
        '403':
          description: Admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign pool unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaigns/{slug}/participation/google:
    post:
      summary: Bind a Google identity to a campaign reservation
      operationId: completeCampaignParticipationGoogleIdentity
      parameters:
        - name: slug
          in: path
          description: Campaign slug or UUID
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignParticipationGoogleIdentityRequest'
      responses:
        '200':
          description: Confirmation email dispatched or idempotent result returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignParticipationGoogleIdentityApiResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Google authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Reservation unavailable or identity already rewarded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '410':
          description: Reservation expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Dispatch failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /campaigns/{slug}/participation/confirm:
    post:
      summary: Confirm a campaign reservation and award Campaign Points
      operationId: confirmCampaignParticipation
      parameters:
        - name: slug
          in: path
          description: Campaign slug or UUID
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignParticipationConfirmationRequest'
      responses:
        '200':
          description: Participation confirmed idempotently
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignParticipationConfirmationApiResponse'
        '400':
          description: Invalid confirmation request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Authenticated identity required for an email reservation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Campaign or reservation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '410':
          description: Confirmation token expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign confirmation conflicts with reservation or reward state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Confirmation failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /contributions/my:
    get:
      summary: Get My Contributions
      operationId: getMyContributions
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyContributionsApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /contributions/my/{id}:
    get:
      summary: Get My Contribution Detail
      operationId: getMyContributionDetail
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Contribution ID
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContributionDetailApiResponse'
        '400':
          description: Invalid contribution ID format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Contribution not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /settings:
    get:
      summary: Get Settings - Collections with Campaign Status
      operationId: getSettings
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SettingsApiResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /overview:
    get:
      summary: Get Overview - User stats and recommendations
      operationId: getOverview
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OverviewApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /overview/point-transactions:
    get:
      summary: List the authenticated user's campaign-point transactions
      operationId: getOverviewPointTransactions
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 20
        - in: query
          name: cursor
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PointTransactionPageApiResponse'
        '400':
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /account:
    get:
      summary: Get Account Information
      operationId: getAccount
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /account/password:
    post:
      summary: Change Password
      operationId: changePassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordRequest'
      responses:
        '200':
          description: Password changed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangePasswordApiResponse'
        '400':
          description: Validation error or incorrect current password
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /profile:
    get:
      summary: Get User Profile
      operationId: getProfile
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Update User Profile
      operationId: updateProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProfileRequest'
      responses:
        '200':
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Email verification or provider re-subscription is required to enable marketing emails
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /aptos/wallet/challenge:
    post:
      summary: Create Aptos Wallet Challenge
      operationId: createAptosWalletChallenge
      responses:
        '200':
          description: Aptos wallet challenge created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AptosWalletChallengeApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Email verification required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /aptos/wallet:
    get:
      summary: Get Aptos Wallet Connection
      operationId: getAptosWalletConnection
      responses:
        '200':
          description: Aptos wallet connection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AptosWalletConnectionApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Link Aptos Wallet
      operationId: linkAptosWallet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LinkAptosWalletRequest'
      responses:
        '200':
          description: Aptos wallet linked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AptosWalletConnectionApiResponse'
        '400':
          description: Invalid Aptos wallet proof
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Email verification required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Wallet already linked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      summary: Disconnect Aptos Wallet
      operationId: disconnectAptosWallet
      responses:
        '200':
          description: Aptos wallet disconnected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AptosWalletConnectionApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/eligibility:
    get:
      summary: Get Creator NFT Eligibility
      operationId: getCreatorEligibility
      responses:
        '200':
          description: Creator eligibility
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorEligibilityApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/dashboard:
    get:
      summary: Get Creator Dashboard
      operationId: getCreatorDashboard
      responses:
        '200':
          description: Creator dashboard
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorDashboardApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/guideline-acceptance:
    post:
      summary: Accept Creator Guideline
      operationId: acceptCreatorGuideline
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatorGuidelineAcceptanceBody'
      responses:
        '200':
          description: Creator guideline accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorGuidelineAcceptanceApiResponse'
        '400':
          description: Creator guideline acceptance is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Failed to save creator guideline acceptance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/stripe-account:
    get:
      summary: Get Creator Stripe Account
      operationId: getCreatorStripeAccount
      responses:
        '200':
          description: Creator Stripe account status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorStripeAccountApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/stripe-account/onboarding-link:
    post:
      summary: Create Creator Stripe Onboarding Link
      operationId: createCreatorStripeOnboardingLink
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatorStripeOnboardingLinkBody'
      responses:
        '200':
          description: Stripe onboarding link
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorStripeOnboardingLinkApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/stripe-account/refresh:
    post:
      summary: Refresh Creator Stripe Account
      operationId: refreshCreatorStripeAccount
      responses:
        '200':
          description: Refreshed Creator Stripe account status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorStripeAccountApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/withdrawals:
    get:
      summary: List Creator Withdrawals
      operationId: listCreatorWithdrawals
      responses:
        '200':
          description: Creator withdrawal history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorWithdrawalListApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Create Creator Withdrawal
      operationId: createCreatorWithdrawal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatorWithdrawalCreateBody'
      responses:
        '200':
          description: Creator withdrawal created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorWithdrawalCreateApiResponse'
        '400':
          description: Invalid withdrawal amount
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Creator eligibility required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Payout account not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/products:
    get:
      summary: List Creator Products
      operationId: listCreatorProducts
      responses:
        '200':
          description: Creator products
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductListApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Create Creator Product Draft
      operationId: createCreatorProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatorProductCreateBody'
      responses:
        '200':
          description: Creator product created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Linked Aptos wallet does not hold an eligible NFT
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Aptos wallet required or creator currency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/custom-digital-products:
    post:
      summary: Create Charged Custom Digital Product Draft
      operationId: createCustomDigitalCreatorProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomDigitalCreatorProductCreateBody'
      responses:
        '200':
          description: Charged custom digital-product draft
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Creator role and eligible NFT required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Aptos wallet required, creator guideline not accepted, scoped points, creation intent, or currency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Custom product creation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/upload:
    post:
      summary: Prepare Campaign or Store Media Upload
      operationId: prepareAdminMediaUpload
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminMediaUploadBody'
      responses:
        '200':
          description: Signed upload target and public media URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminMediaUploadApiResponse'
        '400':
          description: Invalid upload request
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '403':
          description: Upload scope is not allowed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '409':
          description: Creator guideline or Aptos wallet requirement is not met
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '413':
          description: File exceeds the upload size limit
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '415':
          description: Unsupported media type
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '500':
          description: Upload target could not be prepared
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
  /creator/products/{id}/digital-attachments:
    get:
      summary: List Custom Digital Product Attachments
      operationId: listCreatorDigitalAttachments
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Custom product attachment metadata
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminDigitalAttachmentListApiResponse' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
        '409': { description: Creator guideline not accepted }
    post:
      summary: Prepare Custom Digital Product Attachment Upload
      operationId: prepareCreatorDigitalAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DigitalAttachmentPrepareBody' }
      responses:
        '200':
          description: Private upload URL
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DigitalAttachmentPrepareApiResponse' }
        '400': { description: Invalid attachment }
        '401': { description: Unauthorized }
        '403':
          description: Creator role, product ownership, and eligible NFT required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Aptos wallet required or creator guideline not accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/products/{id}/digital-attachments/{attachmentId}:
    delete:
      summary: Delete Custom Digital Product Attachment
      operationId: deleteCreatorDigitalAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: attachmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Attachment logically deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DigitalAttachmentDeleteApiResponse' }
        '401': { description: Unauthorized }
        '403': { description: Forbidden }
        '409': { description: Final attachment conflict }
        '500': { description: Attachment deletion failed }
  /creator/products/{id}/digital-attachments/{attachmentId}/finalize:
    post:
      summary: Finalize Custom Digital Product Attachment Upload
      operationId: finalizeCreatorDigitalAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: attachmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Active attachment metadata
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminDigitalAttachmentApiResponse' }
        '400': { description: Invalid attachment }
        '401': { description: Unauthorized }
        '403':
          description: Creator role, product ownership, and eligible NFT required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404': { description: Pending attachment not found }
        '409':
          description: Aptos wallet required or creator guideline not accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/products/{id}:
    get:
      summary: Get Creator Product
      operationId: getCreatorProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Creator product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Update Creator Product
      operationId: updateCreatorProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatorProductPatchBody'
      responses:
        '200':
          description: Creator product updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Creator role, product ownership, and eligible NFT required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Aptos wallet required, creator guideline not accepted, creator currency conflict, or product is not editable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /creator/products/{id}/submit:
    post:
      summary: Submit Creator Product For Review
      operationId: submitCreatorProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Creator product submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductApiResponse'
        '400':
          description: Invalid status transition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Creator role, product ownership, and eligible NFT required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Aptos wallet required, creator guideline not accepted, or insufficient Mofu Mofu Campaign Collection Points
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /shipping-addresses:
    get:
      summary: List User Shipping Addresses
      operationId: listShippingAddresses
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShippingAddressListApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Create User Shipping Address
      operationId: createShippingAddress
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateShippingAddressBody'
      responses:
        '200':
          description: Shipping address created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShippingAddressApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /shipping-addresses/{id}:
    patch:
      summary: Update User Shipping Address
      operationId: updateShippingAddress
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Shipping address ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchShippingAddressBody'
      responses:
        '200':
          description: Shipping address updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShippingAddressApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Shipping address not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      summary: Delete User Shipping Address
      operationId: deleteShippingAddress
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Shipping address ID
      responses:
        '200':
          description: Shipping address deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteShippingAddressApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Shipping address not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /points:
    get:
      summary: Get User Points Balance
      operationId: getPoints
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PointsApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store:
    get:
      summary: List Active Store Products
      operationId: listStoreProducts
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: >
            Search by product name or description (case-insensitive). Multiple
            keywords separated by spaces are matched independently with AND logic,
            e.g. "blue shirt" matches products containing both "blue" and "shirt"
            in name or description.
        - name: pointsSort
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: asc
          description: Sort by points cost
        - name: priceSort
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
          description: Sort by purchase price
        - name: category
          in: query
          required: false
          schema:
            type: string
            enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          description: Filter by product category
        - name: collection
          in: query
          required: false
          schema:
            type: string
          description: Filter by product collection slug. Empty values are treated as omitted.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
          description: >
            Opt in to a fixed-size, 20-product page. Omitting page preserves the
            legacy unbounded product list response.
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/StoreProductListApiResponse'
                  - $ref: '#/components/schemas/StoreProductPaginatedListApiResponse'
        '400':
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store/{id}:
    get:
      summary: Get Store Product Detail
      operationId: getStoreProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Store product ID
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoreProductApiResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store/collections:
    get:
      summary: List Active Store Product Collections
      operationId: listStoreProductCollections
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoreProductCollectionListApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/campaigns:
    get:
      summary: Admin — List Campaigns
      operationId: adminListCampaigns
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCampaignListApiResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Admin — Create Campaign
      operationId: adminCreateCampaign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminCampaignMutationBody'
      responses:
        '201':
          description: Campaign created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCampaignMutationApiResponse'
        '400':
          description: Invalid campaign input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign slug or Marketplace Collection binding conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/campaign-collections:
    get:
      summary: Admin — List Campaign Collections
      operationId: adminListCampaignCollections
      parameters:
        - name: search
          in: query
          schema:
            type: string
            maxLength: 120
        - name: status
          in: query
          schema:
            type: string
            enum: [ALL, DRAFT, ACTIVE, ARCHIVED]
            default: ALL
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 10
            maximum: 100
            default: 20
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCampaignCollectionListApiResponse'
        '400':
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Admin — Create Campaign Collection
      operationId: adminCreateCampaignCollection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignCollectionCreateBody'
      responses:
        '201':
          description: Campaign Collection created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCampaignCollectionDetailApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign Collection association conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignCollectionAdminErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/campaign-collections/{id}:
    get:
      summary: Admin — Get Campaign Collection
      operationId: adminGetCampaignCollection
      parameters:
        - $ref: '#/components/parameters/CampaignCollectionId'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCampaignCollectionDetailApiResponse'
        '400':
          description: Invalid Campaign Collection ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Campaign Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin — Update Campaign Collection
      operationId: adminUpdateCampaignCollection
      parameters:
        - $ref: '#/components/parameters/CampaignCollectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignCollectionPatchBody'
      responses:
        '200':
          description: Campaign Collection updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCampaignCollectionDetailApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Campaign Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign Collection conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignCollectionAdminErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      summary: Admin — Delete Campaign Collection
      operationId: adminDeleteCampaignCollection
      parameters:
        - $ref: '#/components/parameters/CampaignCollectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignCollectionDeleteBody'
      responses:
        '200':
          description: Campaign Collection deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignCollectionDeleteApiResponse'
        '400':
          description: Invalid input or confirmation conflict
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ApiErrorResponse'
                  - $ref: '#/components/schemas/CampaignCollectionAdminErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Campaign Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign Collection cannot be deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignCollectionAdminErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/campaigns/{id}:
    patch:
      summary: Admin — Update Campaign
      operationId: adminUpdateCampaign
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Campaign ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminCampaignMutationBody'
      responses:
        '200':
          description: Campaign updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCampaignMutationApiResponse'
        '400':
          description: Invalid campaign input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Campaign slug or Marketplace Collection binding conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/collections:
    get:
      summary: Admin — List Store Product Collections
      operationId: adminListStoreProductCollections
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductCollectionListApiResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Admin — Create Store Product Collection
      operationId: adminCreateStoreProductCollection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StoreProductCollectionCreateBody'
      responses:
        '200':
          description: Created collection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductCollectionApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Collection slug already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/collections/{id}:
    get:
      summary: Admin — Get Store Product Collection
      operationId: adminGetStoreProductCollection
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductCollectionApiResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin — Update Store Product Collection
      operationId: adminUpdateStoreProductCollection
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StoreProductCollectionPatchBody'
      responses:
        '200':
          description: Updated collection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductCollectionApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Collection slug already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      summary: Admin — Delete Store Product Collection
      operationId: adminDeleteStoreProductCollection
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Deleted collection ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [success]
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                    required:
                      - id
                required:
                  - status
                  - data
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/collections/landing-page-skill:
    get:
      summary: Admin — Download Collection Landing Page AI Skill
      operationId: adminDownloadStoreCollectionLandingPageSkill
      responses:
        '200':
          description: Canonical Claude Code and Codex authoring skill
          headers:
            Content-Disposition:
              schema:
                type: string
          content:
            text/markdown:
              schema:
                type: string
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/collections/landing-page-block-docs:
    get:
      summary: Admin — Download Collection Landing Page JSON Block Docs
      operationId: adminDownloadStoreCollectionLandingPageBlockDocs
      responses:
        '200':
          description: Canonical collection landing page JSON block reference
          headers:
            Content-Disposition:
              schema:
                type: string
          content:
            text/markdown:
              schema:
                type: string
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/collections/{id}/landing-page:
    get:
      summary: Admin — Get Collection Landing Page Draft
      operationId: adminGetStoreCollectionLandingPage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: locale
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/LandingLocale'
          description: Landing document locale; defaults to the shared "default" document
      responses:
        '200':
          description: Landing page draft and resolved preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCollectionLandingPageApiResponse'
        '400':
          description: Unsupported locale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      summary: Admin — Save Collection Landing Page Draft
      operationId: adminSaveStoreCollectionLandingPage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CollectionLandingDraftSaveBody'
      responses:
        '200':
          description: Saved draft and resolved preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCollectionLandingPageApiResponse'
        '400':
          description: Invalid document or product references
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '409':
          description: Draft revision is stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/collections/{id}/landing-page/publish:
    post:
      summary: Admin — Publish Collection Landing Page Draft
      operationId: adminPublishStoreCollectionLandingPage
      description: >-
        Checks the exact saved revision inside the publication transaction.
        Requires explicit seo.title and seo.description, descriptive block body
        or rich-text content, and alt text for the selected social image when present.
        Incomplete drafts remain saveable. Failed checks return PUBLISH_PRECHECK_FAILED
        with field-specific issues and leave the existing publication unchanged.
        These checks also apply to same-revision retries. Factual content and visual
        review remain separate publication requirements.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CollectionLandingPublishBody'
      responses:
        '200':
          description: Published draft and resolved preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCollectionLandingPageApiResponse'
        '400':
          description: Invalid or missing draft, or failed publication prechecks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '409':
          description: Draft revision is stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store/downloads:
    get:
      summary: Get Current User's Digital Downloads
      operationId: getStoreDownloads
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Products currently downloadable by the signed-in user and their current attachments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DigitalDownloadsApiResponse'
        '401':
          description: Unauthorized or email is not verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: Downloads are temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store/downloads/{productId}/attachments/{attachmentId}/download-url:
    post:
      summary: Create a Short-Lived Digital Attachment Download URL
      operationId: createStoreDownloadUrl
      security:
        - cookieAuth: []
      parameters:
        - name: productId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: attachmentId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Approximately 60-second private storage URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DigitalDownloadUrlApiResponse'
        '401':
          description: Unauthorized or email is not verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product ownership or active attachment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: Private download URL creation is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/collections/{id}/landing-page/unpublish:
    post:
      summary: Admin — Unpublish Collection Landing Page Locale
      operationId: adminUnpublishStoreCollectionLandingPage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CollectionLandingUnpublishBody'
      responses:
        '200':
          description: Localized publication status and current draft state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCollectionLandingUnpublishApiResponse'
        '400':
          description: Invalid input or protected default publication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection or localized publication not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '409':
          description: Publication version is stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LandingPageApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store/redemptions:
    get:
      summary: Get Current User's Redemptions
      operationId: getUserRedemptions
      security:
        - cookieAuth: []
      responses:
        '200':
          description: List of the authenticated user's redemptions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserRedemptionListApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store/redemptions/validate:
    post:
      summary: Validate a Ticket or Merchandise Redemption QR Code
      description: Current admin and operator roles, or a configured validation API key, may preview or fulfill a points redemption for a digital product or merchandise, or redeem a paid digital ticket or on-site pickup order. Role access is revalidated against the current database record. Shipped physical orders cannot be scanned. Orders support partial pickup up to the unrefunded, uncollected quantity; completing pickup marks the order delivered while payment stays PAID. Inventory is already reserved by the redemption or purchase and is not deducted again at pickup.
      operationId: validateRedemptionQrCode
      security:
        - cookieAuth: []
        - storeRedemptionValidateApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRedemptionBody'
      responses:
        '200':
          description: QR code previewed or redeemed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateRedemptionApiResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: QR code not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Redemption already processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /store/{id}/redeem:
    post:
      summary: Redeem a Store Product
      description: Reserves one unit from the selected variant or shared inventory pool and spends points. Merchandise and digital tickets receive a QR code for staff validation. Pickup keeps the stock reservation and does not deduct inventory a second time.
      operationId: redeemStoreProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Product ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedeemProductBody'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedemptionApiResponse'
        '400':
          description: Product not available or out of stock
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Insufficient points
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /cart:
    get:
      summary: Get Active Cart
      operationId: getCart
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CartApiResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /cart/items:
    post:
      summary: Add Item To Cart
      operationId: addCartItem
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddCartItemRequest'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CartApiResponse'
        '400':
          description: Invalid request or product unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Cart was checked out or requested quantity exceeds available stock
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /cart/items/{itemId}:
    patch:
      summary: Update Cart Item Quantity
      operationId: updateCartItem
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Cart item ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCartItemRequest'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CartApiResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Cart item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Cart was checked out or requested quantity exceeds available stock
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      summary: Remove Item From Cart
      operationId: removeCartItem
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Cart item ID
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CartApiResponse'
        '404':
          description: Cart item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Cart was checked out before the item could be removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /cart/checkout:
    post:
      summary: Checkout Cart
      operationId: checkoutCart
      description: >
        Creates a Stripe Checkout session for all items in the active cart.
        For carts containing any non-digital product, shippingAddress is required.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CartCheckoutBody'
      responses:
        '200':
          description: Checkout session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CartCheckoutApiResponse'
        '400':
          description: Empty cart or product unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product variant not found during final reservation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Cart, price, or stock changed before checkout completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /shipping/quote:
    post:
      summary: Quote Shipping
      operationId: quoteShipping
      description: >
        Calculates the applicable shipping line for a cart or single product checkout.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShippingQuoteBody'
      responses:
        '200':
          description: Shipping quote response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShippingQuoteApiResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product variant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Requested quantity exceeds available stock
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /checkout:
    post:
      summary: Start Checkout
      operationId: startCheckout
      description: >
        Creates a Stripe Checkout session for a single product variant purchase.
        For non-digital products, Stripe Checkout collects shipping address and phone number before payment.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutBody'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutSessionApiResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Signed-in user required for private attachment checkout
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Current database email must be verified for private attachment checkout
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product variant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Product stock, price, or final reservation conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /checkout/preview:
    post:
      summary: Preview Checkout Payment Currency and Campaign Points
      operationId: previewCheckout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutPreviewBody'
      responses:
        '200':
          description: Checkout preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutPreviewApiResponse'
        '400':
          description: Invalid checkout preview request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Signed-in user required for private attachment checkout preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Current database email must be verified for private attachment checkout preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Mixed original-currency cart
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: Required FX snapshot is unavailable or stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /currency/jpy-preview:
    get:
      summary: Get Indicative JPY Price Preview
      operationId: getJpyPricePreview
      parameters:
        - name: amountMinorUnits
          in: query
          required: true
          schema: { type: integer, minimum: 0 }
        - name: currency
          in: query
          required: true
          schema: { type: string, enum: [usd, hkd, cny, jpy, krw, twd] }
      responses:
        '200':
          description: Indicative JPY conversion
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JpyPricePreviewApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: FX source unavailable or stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /checkout/resume:
    post:
      summary: Resume Pending Checkout
      operationId: resumeCheckout
      description: >
        Reuses an open Stripe Checkout session for a pending order when available.
        Otherwise creates a new session. For non-digital products, shipping address and phone number are required.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResumeCheckoutBody'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutSessionApiResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Order not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /orders:
    get:
      summary: List User Orders
      operationId: listOrders
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderListApiResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/integrations/lark-order-webhook:
    get:
      summary: Admin - Get Lark Order Webhook Integration
      operationId: getAdminLarkOrderWebhookIntegration
      responses:
        '200':
          description: Lark order webhook integration settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminLarkOrderWebhookIntegrationApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin - Update Lark Order Webhook Integration
      operationId: updateAdminLarkOrderWebhookIntegration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminLarkOrderWebhookIntegrationPatchBody'
      responses:
        '200':
          description: Updated Lark order webhook integration settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminLarkOrderWebhookIntegrationApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/integrations/lark-order-webhook/test:
    post:
      summary: Admin - Test Lark Order Webhook Integration
      operationId: testAdminLarkOrderWebhookIntegration
      responses:
        '200':
          description: Test delivery result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminLarkOrderWebhookIntegrationTestApiResponse'
        '400':
          description: Integration is not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/shipping-rules:
    get:
      summary: Admin - List Shipping Rules
      operationId: listAdminShippingRules
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminShippingRuleListApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Admin - Create Shipping Rule
      operationId: createAdminShippingRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminShippingRuleCreateBody'
      responses:
        '200':
          description: Created shipping rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminShippingRuleApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Duplicate country and currency rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/shipping-rules/{id}:
    patch:
      summary: Admin - Update Shipping Rule
      operationId: updateAdminShippingRule
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminShippingRulePatchBody'
      responses:
        '200':
          description: Updated shipping rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminShippingRuleApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Shipping rule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Duplicate country and currency rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      summary: Admin - Delete Shipping Rule
      operationId: deleteAdminShippingRule
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Deleted shipping rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminShippingRuleApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Shipping rule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/analytics/purchase-amount-by-collection:
    get:
      summary: Admin - Purchase Amount By Collection Analytics
      operationId: adminGetPurchaseAmountByCollectionAnalytics
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPurchaseAmountByCollectionAnalyticsApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/analytics/collections:
    get:
      summary: Admin - List Analytics Collection Summaries
      operationId: adminListAnalyticsCollections
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminAnalyticsCollectionListApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/analytics/collections/{collectionId}:
    get:
      summary: Admin - Get Collection Analytics Overview
      operationId: adminGetCollectionAnalyticsOverview
      parameters:
        - name: collectionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCollectionAnalyticsOverviewApiResponse'
        '400':
          description: Invalid collection id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/analytics/collections/{collectionId}/settings:
    patch:
      summary: Admin - Update Collection Analytics Settings
      operationId: adminUpdateCollectionAnalyticsSettings
      parameters:
        - name: collectionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminCollectionAnalyticsSettingsPatch'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCollectionAnalyticsSettingsApiResponse'
        '400':
          description: Invalid payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/pulse:
    get:
      summary: Admin - Get Overview Pulse
      operationId: adminGetOverviewPulse
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewPulseApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/alerts:
    get:
      summary: Admin - List Overview Alerts
      operationId: adminListOverviewAlerts
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewAlertListApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin - Ack Overview Alert
      operationId: adminAckOverviewAlert
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminOverviewAlertAckPatch'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewAlertAckResultApiResponse'
        '400':
          description: Invalid payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/operations/stripe-fees/retry:
    post:
      summary: Admin - Retry Stripe Fee Synchronization
      operationId: adminRetryOverviewStripeFeeSync
      responses:
        '200':
          description: Synchronization completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewStripeFeeRetryApiResponse'
        '403':
          description: Forbidden - admin role and same-origin request required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Synchronization failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/operations/withdrawals/{withdrawalId}/retry:
    post:
      summary: Admin - Retry Failed Creator Withdrawal
      operationId: adminRetryOverviewWithdrawal
      parameters:
        - name: withdrawalId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Retry dispatched to Stripe
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewWithdrawalRetryApiResponse'
        '400':
          description: Invalid withdrawal id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role and same-origin request required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Withdrawal cannot be safely retried
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/fulfillment:
    get:
      summary: Admin - Get Overview Fulfillment Queues
      operationId: adminGetOverviewFulfillment
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewFulfillmentApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/calendar:
    get:
      summary: Admin - Get Overview Operations Calendar
      operationId: adminGetOverviewCalendar
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewCalendarApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/market:
    get:
      summary: Admin - Get Overview Market
      operationId: adminGetOverviewMarket
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewMarketApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/community:
    get:
      summary: Admin - Get Overview Community
      operationId: adminGetOverviewCommunity
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewCommunityApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/points:
    get:
      summary: Admin - Get Overview Points Economy
      operationId: adminGetOverviewPoints
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOverviewPointsApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/overview/settings:
    get:
      summary: Admin - Get Platform Overview Settings
      operationId: adminGetOverviewSettings
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPlatformOverviewSettingsApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin - Update Platform Overview Settings
      operationId: adminUpdateOverviewSettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminPlatformOverviewSettingsPatch'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPlatformOverviewSettingsApiResponse'
        '400':
          description: Invalid payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/finance/collection-earnings:
    get:
      summary: Admin - Get Stripe Collection Earnings
      operationId: adminGetStripeCollectionEarnings
      parameters:
        - name: collectionId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: currency
          in: query
          required: false
          schema:
            type: string
            pattern: '^[A-Za-z]{3}$'
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            enum: [25, 50, 100]
            default: 50
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStripeCollectionEarningsApiResponse'
        '400':
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /cron/stripe-collection-earnings:
    get:
      summary: Sync Stripe Collection Earnings
      operationId: cronSyncStripeCollectionEarnings
      security:
        - cronSecretBearer: []
      responses:
        '200':
          description: Stripe collection earnings sync completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeCollectionEarningsSyncApiResponse'
        '401':
          description: Missing or invalid Cron secret
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CronErrorResponse'
        '500':
          description: Stripe collection earnings sync failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CronErrorResponse'
  /admin/store/redemptions:
    get:
      summary: Admin — List Point Redemptions
      operationId: adminListStoreRedemptions
      security:
        - cookieAuth: []
      parameters:
        - name: search
          in: query
          required: false
          schema: { type: string }
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [ALL, FULFILLED, CANCELLED]
            default: ALL
        - name: page
          in: query
          required: false
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        '200':
          description: Point-redemption list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPointRedemptionListApiResponse'
        '400':
          description: Invalid query
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '500':
          description: Point-redemption list failed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
  /admin/store/redemptions/{redemptionId}/cancel:
    post:
      summary: Admin — Cancel a Point Redemption
      operationId: adminCancelStoreRedemption
      security:
        - cookieAuth: []
      parameters:
        - name: redemptionId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminPointRedemptionCancelBody'
      responses:
        '200':
          description: Redemption cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPointRedemptionApiResponse'
        '400':
          description: Invalid reason or non-attachment redemption
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '404':
          description: Redemption not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '500':
          description: Point-redemption cancellation failed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
  /admin/orders:
    get:
      summary: Admin - List Order Sessions
      operationId: adminListOrderSessions
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Search by checkout session, payment intent, customer, recipient, or product
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [ALL, PENDING, PAID, DELIVERED, FAILED, REFUNDED]
            default: ALL
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [7day, 30day, 1year, all]
            default: all
          description: Filter to order sessions created during the selected period
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            enum: [20, 50, 100]
            default: 20
        - name: collectionId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter to order sessions containing at least one product in this collection
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderSessionListApiResponse'
        '400':
          description: Invalid query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/orders/export:
    get:
      summary: Admin - Export Order Sessions CSV
      operationId: adminExportOrderSessionsCsv
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Search by checkout session, payment intent, customer, recipient, or product
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [ALL, PENDING, PAID, DELIVERED, FAILED, REFUNDED]
            default: ALL
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [7day, 30day, 1year, all]
            default: all
          description: Filter to order sessions created during the selected period
        - name: collectionId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter to order sessions containing at least one product in this collection
      responses:
        '200':
          description: CSV file containing all matching order sessions
          content:
            text/csv:
              schema:
                type: string
        '400':
          description: Invalid query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/orders/{sessionId}:
    patch:
      summary: Admin - Update Order Session
      operationId: adminUpdateOrderSession
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
          description: Stripe Checkout Session ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminOrderSessionActionBody'
      responses:
        '200':
          description: Updated order session. For resend_purchase_confirmation, this means the provider accepted the request or the same request UUID replayed an accepted send; it does not guarantee delivery or inbox placement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderSessionApiResponse'
        '400':
          description: Invalid input or unsupported transition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required or same-origin CSRF check failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Order session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Order is ineligible or another resend is already in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '429':
          description: A previous resend was accepted within the application cooldown window
          headers:
            Retry-After:
              description: Seconds until another request may be accepted
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '502':
          description: Email provider rejected the resend or returned no message id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: Email provider is unavailable, rate-limited, timed out, or not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store:
    get:
      summary: Admin — List All Store Products
      operationId: adminListStoreProducts
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Search by product name, description, or collection name
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum: [date_asc, date_desc]
            default: date_desc
          description: Sort by product created date
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number (1-based)
        - name: collectionId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by product collection ID
        - name: creatorOnly
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Filter to NFT creator-submitted products
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/CreatorProductStatus'
          description: Filter by product status
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoreProductListApiResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Admin — Create Store Product
      operationId: adminCreateStoreProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StoreProductCreateBody'
      responses:
        '200':
          description: Created product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductApiResponse'
        '400':
          description: Invalid input, including an ACTIVE product with no ACTIVE variants. Inactive drafts and active variants with zero stock are allowed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: URL handle already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/export:
    get:
      summary: Admin — Export Store Products CSV
      operationId: adminExportStoreProductsCsv
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Search by product name, description, or collection name
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum: [date_asc, date_desc]
            default: date_desc
          description: Sort by product created date
        - name: collectionId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by product collection ID
      responses:
        '200':
          description: CSV file containing all matching store products
          content:
            text/csv:
              schema:
                type: string
        '400':
          description: Invalid query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/reorder:
    post:
      summary: Admin — Reorder Store Products
      operationId: adminReorderStoreProducts
      description: >
        Persist the curated featured store product order for a collection.
        When `collectionId` is supplied, the payload is a complete top-first
        snapshot for that collection only; products omitted from `orderedIds`
        are removed from that collection's curated set and sort after curated
        products in the same collection.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StoreReorderBody'
      responses:
        '200':
          description: Store product order updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoreReorderApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: One or more products no longer exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/orders/{sessionId}/refund-preview:
    get:
      summary: Admin - Preview Full Order Refund
      operationId: adminGetOrderRefundPreview
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Server-authoritative full order refund amounts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderRefundPreviewApiResponse'
        '400':
          description: Order cannot be refunded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Order session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/refund-fee:
    get:
      summary: Admin - Get Refund Service Fee
      operationId: adminGetRefundFee
      responses:
        '200':
          description: Refund service fee configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefundFeeConfigApiResponse'
        '403':
          description: Forbidden - admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin - Update Refund Service Fee
      operationId: adminUpdateRefundFee
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefundFeeConfigPatchBody'
      responses:
        '200':
          description: Refund service fee configuration updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefundFeeConfigApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden - admin role required or same-origin CSRF check failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/creator-fees:
    get:
      summary: Admin — Get Creator Fee Config
      operationId: adminGetCreatorFees
      responses:
        '200':
          description: Creator fee config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorFeeConfigApiResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin — Update Creator Fee Config
      operationId: adminUpdateCreatorFees
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatorFeeConfigPatchBody'
      responses:
        '200':
          description: Creator fee config updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorFeeConfigApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/tags:
    get:
      summary: Admin — List Store Product Tags
      operationId: adminListStoreProductTags
      parameters:
        - name: query
          in: query
          required: false
          schema:
            type: string
          description: Canonicalized prefix for tag suggestions.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 20
          description: Maximum number of suggestions to return.
      responses:
        '200':
          description: Canonical product tag suggestions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductTagsApiResponse'
        '400':
          description: Invalid query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/{id}:
    get:
      summary: Admin — Get Store Product
      operationId: adminGetStoreProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Product ID
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductApiResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      summary: Admin — Update Store Product
      operationId: adminUpdateStoreProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Product ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StoreProductPatchBody'
      responses:
        '200':
          description: Updated product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminStoreProductApiResponse'
        '400':
          description: Invalid input, including an ACTIVE product with no ACTIVE variants. Inactive drafts and active variants with zero stock are allowed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: URL handle already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      summary: Admin — Delete Store Product
      operationId: adminDeleteStoreProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Product ID
      responses:
        '200':
          description: Deleted product ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [success]
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                    required:
                      - id
                required:
                  - status
                  - data
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/{id}/digital-attachments:
    get:
      summary: Admin — List private digital attachments
      operationId: adminListDigitalAttachments
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Sanitized attachment metadata for the product editor
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminDigitalAttachmentListApiResponse' }
        '400':
          description: Product is not eligible for attachment delivery
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '404':
          description: Product not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
    post:
      summary: Admin — Prepare private digital attachment upload
      operationId: adminPrepareDigitalAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DigitalAttachmentPrepareBody' }
      responses:
        '200':
          description: A one-time signed storage URL for an exact private object key
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DigitalAttachmentPrepareApiResponse' }
        '400':
          description: Invalid attachment or ineligible product
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '404':
          description: Product or replacement attachment not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
  /admin/store/{id}/digital-attachments/{attachmentId}:
    delete:
      summary: Admin — Delete private digital attachment
      operationId: adminDeleteDigitalAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: attachmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Attachment logically deleted; repeated calls retry cleanup safely
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DigitalAttachmentDeleteApiResponse' }
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '500':
          description: Cleanup state could not be persisted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
  /admin/store/{id}/digital-attachments/{attachmentId}/finalize:
    post:
      summary: Admin — Finalize private digital attachment upload
      operationId: adminFinalizeDigitalAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: attachmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Validated active attachment metadata
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminDigitalAttachmentApiResponse' }
        '400':
          description: Uploaded object failed server-side validation or replacement could not be applied
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '404':
          description: Pending attachment not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
  /admin/store/{id}/digital-attachments/{attachmentId}/inspect:
    get:
      summary: Admin — Inspect a private digital attachment
      operationId: adminInspectDigitalAttachment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: attachmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '302':
          description: Redirect to a 60-second private storage URL; no URL is returned in an application JSON response
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
        '404':
          description: Active attachment not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiErrorResponse' }
  /admin/store/{id}/approve-creator-product:
    post:
      summary: Creator-product reviewer — Approve Creator Product
      operationId: adminApproveCreatorProduct
      description: "The sole /api/admin/** review exception: current admin and operator roles may approve a creator-owned product that is pending review. All other admin routes require admin."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Creator product approved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductApiResponse'
        '400':
          description: Invalid review action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — current admin or operator role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Pending creator product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Creator product pricing conflicts with its current promotion
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/{id}/reject-creator-product:
    post:
      summary: Creator-product reviewer — Reject Creator Product
      operationId: adminRejectCreatorProduct
      description: "The sole /api/admin/** review exception: current admin and operator roles may reject a creator-owned product that is pending review. All other admin routes require admin."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminCreatorProductReviewBody'
      responses:
        '200':
          description: Creator product rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatorProductApiResponse'
        '400':
          description: Invalid review action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — current admin or operator role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Pending creator product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/store/bulk:
    post:
      summary: Admin — Bulk action on Store Products
      operationId: adminBulkStoreAction
      description: >
        Apply a single action ("create", "update", or "delete") to up to 100
        store products in one request. For "create", provide `products`
        and optionally a top-level `collectionId` to assign by default. For
        "update", supply a `patch` object with any subset of mutable fields;
        the same patch is applied to every id. For "delete", linked Stripe
        products are archived first (best-effort) and dependent rows (orders,
        redemptions, cart items) are removed via FK cascade.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StoreBulkActionBody'
      responses:
        '200':
          description: Result of the bulk action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoreBulkActionApiResponse'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /leaderboard:
    get:
      summary: Get Threads Leaderboard
      operationId: getLeaderboard
      parameters:
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: >
            Filter leaderboard to posts from participants of a specific campaign.
            If omitted, returns posts across all campaigns.
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LeaderboardApiResponse'
        '400':
          description: Invalid campaignId format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/marketing-email-drafts:
    get:
      summary: Admin — List marketing email drafts
      operationId: adminListMarketingEmailDrafts
      responses:
        '200':
          description: Marketing email drafts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketingEmailDraftListApiResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      summary: Admin — Create a marketing email draft
      operationId: adminCreateMarketingEmailDraft
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarketingEmailDraftInput'
      responses:
        '200':
          description: Draft created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketingEmailDraftApiResponse'
        '400':
          description: Invalid or unsafe draft input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden or invalid request origin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: Request body exceeds the byte limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/marketing-email-drafts/{id}:
    get:
      summary: Admin — Get a marketing email draft
      operationId: adminGetMarketingEmailDraft
      parameters:
        - $ref: '#/components/parameters/MarketingEmailDraftId'
      responses:
        '200':
          description: Marketing email draft
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketingEmailDraftDetailApiResponse'
        '400':
          description: Invalid draft ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden — admin role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Draft not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      summary: Admin — Save a marketing email draft
      operationId: adminSaveMarketingEmailDraft
      parameters:
        - $ref: '#/components/parameters/MarketingEmailDraftId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarketingEmailDraftSaveBody'
      responses:
        '200':
          description: Draft saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketingEmailDraftApiResponse'
        '400':
          description: Invalid or unsafe draft input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden or invalid request origin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Draft not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Draft revision is stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: Request body exceeds the byte limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/marketing-email-drafts/{id}/preview:
    post:
      summary: Admin — Get a sandboxed marketing email preview snapshot
      operationId: adminPreviewMarketingEmailDraft
      parameters:
        - $ref: '#/components/parameters/MarketingEmailDraftId'
      responses:
        '200':
          description: Canonical rendered HTML snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketingEmailPreviewApiResponse'
        '400':
          description: Invalid draft ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden or invalid request origin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Draft not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/marketing-email-drafts/{id}/test-send:
    post:
      summary: Admin — Send a self-only marketing email test
      operationId: adminSendMarketingEmailDraftTest
      parameters:
        - $ref: '#/components/parameters/MarketingEmailDraftId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarketingEmailTestSendBody'
      responses:
        '200':
          description: Test accepted by the configured provider
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketingEmailTestSendApiResponse'
        '400':
          description: Invalid draft ID or request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Forbidden or invalid request origin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Draft not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Draft is incomplete or stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: Request body exceeds the byte limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '502':
          description: Provider rejected the test
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: Test provider is not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Error response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /admin/marketing-email-drafts/{id}/send-runs:
    post:
      summary: Admin — Snapshot a marketing send run and queue server-side preparation
      operationId: adminCreateMarketingEmailSendRun
      parameters:
        - $ref: '#/components/parameters/MarketingEmailDraftId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketingEmailCreateSendRunBody' }
      responses:
        '200': { description: Recipient snapshot captured and preparation queued, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailSendRunCreateApiResponse' } } } }
        '400': { description: Invalid input, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '404': { description: Draft not found, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '409':
          description: Active or stale send run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413': { description: Request body exceeds the byte limit, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '422': { description: Send is not eligible, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Error response, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '503': { description: Sending is disabled or unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /admin/marketing-email-send-runs:
    get:
      summary: Admin — List marketing send runs without recipient PII
      operationId: adminListMarketingEmailSendRuns
      responses:
        '200': { description: Send runs, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailSendRunListApiResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Error response, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /admin/marketing-email-send-runs/{runId}:
    get:
      summary: Admin — Get immutable marketing send run preview and audit state
      operationId: adminGetMarketingEmailSendRun
      parameters:
        - $ref: '#/components/parameters/MarketingEmailSendRunId'
      responses:
        '200': { description: Send run detail, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailSendRunDetailApiResponse' } } } }
        '400': { description: Invalid send run ID, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '404': { description: Send run not found, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Error response, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /admin/marketing-email-send-runs/{runId}/confirmation-challenge:
    post:
      summary: Admin — Refresh a five-minute send confirmation challenge
      operationId: adminCreateMarketingEmailSendConfirmationChallenge
      parameters:
        - $ref: '#/components/parameters/MarketingEmailSendRunId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketingEmailEmptyBody' }
      responses:
        '200': { description: Challenge created, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailConfirmationApiResponse' } } } }
        '400': { description: Invalid input, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '409':
          description: Run blocked or stale
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413': { description: Request body too large, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Confirmation challenge failed, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '502': { description: Marketing email provider rejected the challenge check, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '503': { description: Marketing email sending is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /admin/marketing-email-send-runs/{runId}/confirm:
    post:
      summary: Admin — Confirm a marketing send run for durable submission
      operationId: adminConfirmMarketingEmailSendRun
      parameters:
        - $ref: '#/components/parameters/MarketingEmailSendRunId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketingEmailConfirmBody' }
      responses:
        '200': { description: Durable job queued, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailSendRunApiResponse' } } } }
        '400': { description: Invalid input, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '404': { description: Send run not found, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '409':
          description: Token stale, expired, or run blocked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413': { description: Request body too large, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '422': { description: Send run cannot be confirmed, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Confirmation failed, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '503': { description: Marketing email sending is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /admin/marketing-email-send-runs/{runId}/resume:
    post:
      summary: Admin — Recover a known send run without creating another broadcast
      operationId: adminResumeMarketingEmailSendRun
      parameters:
        - $ref: '#/components/parameters/MarketingEmailSendRunId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketingEmailEmptyBody' }
      responses:
        '200': { description: Recovered send run, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailSendRunApiResponse' } } } }
        '400': { description: Invalid input, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '404': { description: Send run not found, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Error response, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '409':
          description: Recovery needs operator review
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413': { description: Request body too large, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '422': { description: Send run cannot be resumed safely, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '502': { description: Marketing email provider rejected the recovery operation, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '503': { description: Marketing email sending is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /admin/marketing-email-webhook-health:
    get:
      summary: Admin — Get aggregate Resend webhook health
      operationId: adminGetMarketingEmailWebhookHealth
      responses:
        '200': { description: Aggregate webhook health, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailWebhookHealthApiResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Error response, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /webhooks/resend:
    post:
      summary: Receive a signed Resend webhook
      operationId: resendMarketingEmailWebhook
      parameters:
        - name: svix-id
          in: header
          required: true
          schema: { type: string }
        - name: svix-timestamp
          in: header
          required: true
          schema: { type: string }
        - name: svix-signature
          in: header
          required: true
          schema: { type: string }
      requestBody:
        required: true
        description: Exact JSON payload bytes used for Svix signature verification
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200': { description: Webhook received, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailWebhookReceipt' } } } }
        '400': { description: Invalid webhook signature, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Webhook processing failed, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '503': { description: Webhook verification not configured, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /cron/marketing-email-worker:
    get:
      summary: Drain marketing contact outbox and submit durable send jobs
      operationId: cronRunMarketingEmailWorker
      security:
        - cronSecretBearer: []
      responses:
        '200': { description: Worker completed, content: { application/json: { schema: { $ref: '#/components/schemas/MarketingEmailWorkerApiResponse' } } } }
        '401': { description: Missing or invalid Cron secret, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Worker failed, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
  /cron/checkout-probe:
    get:
      summary: Run the production checkout probe
      operationId: cronRunCheckoutProbe
      security:
        - cronSecretBearer: []
      responses:
        '200': { description: Checkout probe completed, content: { application/json: { schema: { $ref: '#/components/schemas/CheckoutProbeApiResponse' } } } }
        '401': { description: Missing or invalid Cron secret, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
        '500': { description: Checkout probe failed, content: { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } } }
components:
  parameters:
    CampaignCollectionId:
      name: id
      in: path
      required: true
      description: Campaign Collection ID
      schema:
        type: string
        format: uuid
    MarketingEmailDraftId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Marketing email draft ID
    MarketingEmailSendRunId:
      name: runId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Marketing email send run ID
  securitySchemes:
    storeRedemptionValidateApiKey:
      type: http
      scheme: bearer
      description: Bearer token matching STORE_REDEMPTION_VALIDATE_API_KEY.
    cronSecretBearer:
      type: http
      scheme: bearer
      description: Bearer token matching CRON_SECRET.
  schemas:
    # Standard API Response Wrappers
    MyContributionsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/ContributionSummary'
      required:
        - status
        - data
    AccountApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AccountData'
      required:
        - status
        - data
    ChangePasswordRequest:
      type: object
      properties:
        currentPassword:
          type: string
          description: The user's current password
        newPassword:
          type: string
          minLength: 8
          description: The new password (minimum 8 characters)
        confirmPassword:
          type: string
          description: Must match newPassword
      required:
        - currentPassword
        - newPassword
        - confirmPassword
    ChangePasswordApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            message:
              type: string
            code:
              type: string
              enum: [price_updated]
          required:
            - message
      required:
        - status
        - data
    AptosSignInInput:
      type: object
      properties:
        domain:
          type: string
        nonce:
          type: string
        address:
          type: string
        uri:
          type: string
        version:
          type: string
        statement:
          type: string
        chainId:
          type: string
        issuedAt:
          type: string
          format: date-time
        expirationTime:
          type: string
          format: date-time
        notBefore:
          type: string
          format: date-time
        requestId:
          type: string
        resources:
          type: array
          items:
            type: string
      required:
        - domain
        - nonce
    AptosWalletChallengeData:
      type: object
      properties:
        input:
          $ref: '#/components/schemas/AptosSignInInput'
      required:
        - input
    AptosWalletChallengeApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AptosWalletChallengeData'
      required:
        - status
        - data
    SerializedAptosSignInOutput:
      type: object
      properties:
        version:
          type: string
          enum: ['3']
        type:
          type: string
        signature:
          type: string
        input:
          $ref: '#/components/schemas/AptosSignInInput'
        publicKey:
          type: string
      required:
        - version
        - type
        - signature
        - input
        - publicKey
    LinkAptosWalletRequest:
      type: object
      properties:
        output:
          $ref: '#/components/schemas/SerializedAptosSignInOutput'
      required:
        - output
    AptosWalletConnectionData:
      type: object
      properties:
        address:
          type: string
          nullable: true
        connectedAt:
          type: string
          format: date-time
          nullable: true
      required:
        - address
        - connectedAt
    AptosWalletConnectionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AptosWalletConnectionData'
      required:
        - status
        - data
    CreatorGuidelineAcceptanceBody:
      type: object
      properties:
        accepted:
          type: boolean
          enum: [true]
      required:
        - accepted
    CreatorGuidelineAcceptanceData:
      type: object
      properties:
        accepted:
          type: boolean
        acceptedAt:
          type: string
          format: date-time
          nullable: true
        documentName:
          type: string
        documentPath:
          type: string
        title:
          type: string
        version:
          type: string
      required:
        - accepted
        - acceptedAt
        - documentName
        - documentPath
        - title
        - version
    CreatorGuidelineAcceptanceApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatorGuidelineAcceptanceData'
      required:
        - status
        - data
    UpdateProfileRequest:
      type: object
      properties:
        preferredLanguage:
          type: string
          enum: [en, ja, ko, zh-TW, zh-CN]
        marketingEmailsEnabled:
          type: boolean
      anyOf:
        - required: [preferredLanguage]
        - required: [marketingEmailsEnabled]
    ProfileApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/ProfileData'
      required:
        - status
        - data
    ShippingAddressApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/ShippingAddress'
      required:
        - status
        - data
    ShippingAddressListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/ShippingAddress'
      required:
        - status
        - data
    DeleteShippingAddressApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
          required:
            - id
      required:
        - status
        - data
    PointsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/PointsData'
      required:
        - status
        - data
    LeaderboardApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/LeaderboardPost'
      required:
        - status
        - data
    AdminOverviewCurrencyWindow:
      type: object
      properties:
        currency:
          type: string
        revenueCents:
          type: number
        units:
          type: number
        orderCount:
          type: number
      required: [currency, revenueCents, units, orderCount]
    AdminOverviewDailyBucket:
      type: object
      properties:
        day:
          type: string
        currency:
          type: string
        revenueCents:
          type: number
        units:
          type: number
      required: [day, currency, revenueCents, units]
    AdminOverviewMonthToDate:
      type: object
      properties:
        currency:
          type: string
        revenueCents:
          type: number
        targetCents:
          type: number
          nullable: true
      required: [currency, revenueCents, targetCents]
    AdminOverviewPulseLeadingPost:
      type: object
      properties:
        permalink:
          type: string
        content:
          type: string
          nullable: true
        likeCount:
          type: number
      required: [permalink, content, likeCount]
    AdminOverviewPulseActions:
      type: object
      properties:
        shippingBacklog:
          type: number
        shippingOldestDays:
          type: number
          nullable: true
        pickupBacklog:
          type: number
      required: [shippingBacklog, shippingOldestDays, pickupBacklog]
    AdminOverviewPulseLeading:
      type: object
      properties:
        yesterdayClicks:
          type: number
        priorDayClicks:
          type: number
        yesterdayPosts:
          type: number
        priorDayPosts:
          type: number
        topPosts:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewPulseLeadingPost'
      required: [yesterdayClicks, priorDayClicks, yesterdayPosts, priorDayPosts, topPosts]
    AdminOverviewPulse:
      type: object
      properties:
        timezone:
          type: string
        today:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewCurrencyWindow'
        yesterday:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewCurrencyWindow'
        lastWeekSameDay:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewCurrencyWindow'
        monthToDate:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewMonthToDate'
        daily14d:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewDailyBucket'
        actions:
          $ref: '#/components/schemas/AdminOverviewPulseActions'
        leading:
          $ref: '#/components/schemas/AdminOverviewPulseLeading'
      required: [timezone, today, yesterday, lastWeekSameDay, monthToDate, daily14d, actions, leading]
    AdminOverviewAlert:
      type: object
      properties:
        key:
          type: string
        rule:
          type: string
          enum: [PENDING_STALE, DIGITAL_UNDELIVERED, PAYMENT_FEE_ERROR, PAYMENT_FEE_MISSING, SYNC_FAILED, REFUND_SPIKE, SELLOUT_SOON, WITHDRAWAL_STUCK, REDEMPTION_STALE, OUT_OF_PERIOD_ORDER]
        severity:
          type: string
          enum: [RED, YELLOW]
        count:
          type: number
        entityLabel:
          type: string
          nullable: true
        href:
          type: string
        ackStatus:
          type: string
          enum: [DONE, IGNORED]
          nullable: true
      required: [key, rule, severity, count, entityLabel, href, ackStatus]
    AdminOverviewAlertList:
      type: object
      properties:
        alerts:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewAlert'
      required: [alerts]
    AdminOverviewAlertAckPatch:
      type: object
      properties:
        key:
          type: string
        status:
          type: string
          enum: [DONE, IGNORED]
          nullable: true
      required: [key, status]
      additionalProperties: false
    AdminOverviewAlertAckResult:
      type: object
      properties:
        key:
          type: string
        status:
          type: string
          enum: [DONE, IGNORED]
          nullable: true
      required: [key, status]
    AdminOverviewFulfillmentRow:
      type: object
      properties:
        orderId:
          type: string
        productName:
          type: string
        variantName:
          type: string
        quantity:
          type: number
        paidAt:
          type: string
          nullable: true
        daysWaiting:
          type: number
      required: [orderId, productName, variantName, quantity, paidAt, daysWaiting]
    AdminOverviewPickListRow:
      type: object
      properties:
        productName:
          type: string
        variantName:
          type: string
        units:
          type: number
      required: [productName, variantName, units]
    AdminOverviewFulfillmentShipping:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewFulfillmentRow'
        backlogCount:
          type: number
        oldestDays:
          type: number
          nullable: true
        pickList:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewPickListRow'
      required: [rows, backlogCount, oldestDays, pickList]
    AdminOverviewFulfillmentPickup:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewFulfillmentRow'
        backlogCount:
          type: number
        oldestDays:
          type: number
          nullable: true
      required: [rows, backlogCount, oldestDays]
    AdminOverviewFulfillment:
      type: object
      properties:
        shipping:
          $ref: '#/components/schemas/AdminOverviewFulfillmentShipping'
        pickup:
          $ref: '#/components/schemas/AdminOverviewFulfillmentPickup'
      required: [shipping, pickup]
    AdminOverviewCalendarEvent:
      type: object
      properties:
        date:
          type: string
        type:
          type: string
          enum: [EARLY_BIRD_START, EARLY_BIRD_END, PERIOD_START, PERIOD_END, CAMPAIGN_START, CAMPAIGN_END]
        label:
          type: string
        daysFromToday:
          type: number
        href:
          type: string
          nullable: true
      required: [date, type, label, daysFromToday, href]
    AdminOverviewCalendarWarning:
      type: object
      properties:
        collectionId:
          type: string
        collectionName:
          type: string
        type:
          type: string
          enum: [NO_ACTIVE_TICKET]
      required: [collectionId, collectionName, type]
    AdminOverviewCalendar:
      type: object
      properties:
        timezone:
          type: string
        horizonDays:
          type: number
        events:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewCalendarEvent'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewCalendarWarning'
      required: [timezone, horizonDays, events, warnings]
    AdminOverviewRiverDay:
      type: object
      properties:
        day:
          type: string
        currency:
          type: string
        revenueCents:
          type: number
        units:
          type: number
      required: [day, currency, revenueCents, units]
    AdminOverviewRiverBuyers:
      type: object
      properties:
        day:
          type: string
        buyerCount:
          type: number
      required: [day, buyerCount]
    AdminOverviewRiverCollectionBand:
      type: object
      properties:
        collectionId:
          type: string
        name:
          type: string
        periodStart:
          type: string
        periodEnd:
          type: string
          nullable: true
        isTicketCollection:
          type: boolean
      required: [collectionId, name, periodStart, periodEnd, isTicketCollection]
    AdminOverviewRiverCampaignBand:
      type: object
      properties:
        campaignId:
          type: string
        title:
          type: string
        startDate:
          type: string
        endDate:
          type: string
      required: [campaignId, title, startDate, endDate]
    AdminOverviewRiver:
      type: object
      properties:
        days:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewRiverDay'
        buyers:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewRiverBuyers'
        collectionBands:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewRiverCollectionBand'
        campaignBands:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewRiverCampaignBand'
      required: [days, buyers, collectionBands, campaignBands]
    AdminOverviewHeatSku:
      type: object
      properties:
        productName:
          type: string
        variantName:
          type: string
        units30d:
          type: number
        remainingQuantity:
          type: number
          nullable: true
      required: [productName, variantName, units30d, remainingQuantity]
    AdminOverviewHeatAmount:
      type: object
      properties:
        currency:
          type: string
        revenueCents:
          type: number
      required: [currency, revenueCents]
    AdminOverviewHeatSparkDay:
      type: object
      properties:
        day:
          type: string
        units:
          type: number
      required: [day, units]
    AdminOverviewHeatRow:
      type: object
      properties:
        collectionId:
          type: string
        name:
          type: string
        slug:
          type: string
        coverImageUrl:
          type: string
          nullable: true
        isTicketCollection:
          type: boolean
        units30d:
          type: number
        prevUnits30d:
          type: number
        amounts30d:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewHeatAmount'
        sharePermille:
          type: number
        spark:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewHeatSparkDay'
        topSkus:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewHeatSku'
      required: [collectionId, name, slug, coverImageUrl, isTicketCollection, units30d, prevUnits30d, amounts30d, sharePermille, spark, topSkus]
    AdminOverviewHeat:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewHeatRow'
      required: [rows]
    AdminOverviewMarket:
      type: object
      properties:
        timezone:
          type: string
        river:
          $ref: '#/components/schemas/AdminOverviewRiver'
        heat:
          $ref: '#/components/schemas/AdminOverviewHeat'
      required: [timezone, river, heat]
    AdminOverviewFunnelCounts:
      type: object
      properties:
        linkedAccounts:
          type: number
        participants:
          type: number
        contributors:
          type: number
        clicks:
          type: number
        buyers:
          type: number
        firstTimeBuyers:
          type: number
        repeatBuyers:
          type: number
      required: [linkedAccounts, participants, contributors, clicks, buyers, firstTimeBuyers, repeatBuyers]
    AdminOverviewCampaignClicks:
      type: object
      properties:
        campaignId:
          type: string
        title:
          type: string
        clicks30d:
          type: number
      required: [campaignId, title, clicks30d]
    AdminOverviewFunnel:
      type: object
      properties:
        allTime:
          $ref: '#/components/schemas/AdminOverviewFunnelCounts'
        recent30d:
          $ref: '#/components/schemas/AdminOverviewFunnelCounts'
        posts30d:
          type: number
        campaignClicks:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewCampaignClicks'
      required: [allTime, recent30d, posts30d, campaignClicks]
    AdminOverviewVoiceDailyPosts:
      type: object
      properties:
        day:
          type: string
        posts:
          type: number
        creators:
          type: number
      required: [day, posts, creators]
    AdminOverviewVoiceTopPost:
      type: object
      properties:
        postId:
          type: string
        permalink:
          type: string
        content:
          type: string
          nullable: true
        likeCount:
          type: number
        replyCount:
          type: number
        repostCount:
          type: number
        username:
          type: string
          nullable: true
      required: [postId, permalink, content, likeCount, replyCount, repostCount, username]
    AdminOverviewVoiceTopCreator:
      type: object
      properties:
        userId:
          type: string
        name:
          type: string
        posts30d:
          type: number
        engagement30d:
          type: number
        clicks30d:
          type: number
        isNew:
          type: boolean
      required: [userId, name, posts30d, engagement30d, clicks30d, isNew]
    AdminOverviewVoiceConcentration:
      type: object
      properties:
        headPermille:
          type: number
        midPermille:
          type: number
        tailPermille:
          type: number
      required: [headPermille, midPermille, tailPermille]
    AdminOverviewVoice:
      type: object
      properties:
        interactions7d:
          type: number
        interactions30d:
          type: number
        snapshotFreshAt:
          type: string
          nullable: true
        dailyPosts:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewVoiceDailyPosts'
        newCreators30d:
          type: number
        topPosts:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewVoiceTopPost'
        topCreators:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewVoiceTopCreator'
        concentration:
          $ref: '#/components/schemas/AdminOverviewVoiceConcentration'
      required: [interactions7d, interactions30d, snapshotFreshAt, dailyPosts, newCreators30d, topPosts, topCreators, concentration]
    AdminOverviewSupplyMonth:
      type: object
      properties:
        month:
          type: string
        thirdPartyCreators:
          type: number
        officialCollections:
          type: number
      required: [month, thirdPartyCreators, officialCollections]
    AdminOverviewSupplyWeekCount:
      type: object
      properties:
        week:
          type: string
        count:
          type: number
      required: [week, count]
    AdminOverviewSupplyRevenueStats:
      type: object
      properties:
        currency:
          type: string
        creatorCount:
          type: number
        medianCents:
          type: number
        top10SharePermille:
          type: number
      required: [currency, creatorCount, medianCents, top10SharePermille]
    AdminOverviewSupplyCreatorRef:
      type: object
      properties:
        userId:
          type: string
        name:
          type: string
      required: [userId, name]
    AdminOverviewSupply:
      type: object
      properties:
        monthlySellers:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewSupplyMonth'
        weeklyNewActiveProducts:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewSupplyWeekCount'
        payoutReadyAccounts:
          type: number
        revenueStats:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewSupplyRevenueStats'
        zeroRevenueCreators:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewSupplyCreatorRef'
      required: [monthlySellers, weeklyNewActiveProducts, payoutReadyAccounts, revenueStats, zeroRevenueCreators]
    AdminOverviewCommunity:
      type: object
      properties:
        funnel:
          $ref: '#/components/schemas/AdminOverviewFunnel'
        voice:
          $ref: '#/components/schemas/AdminOverviewVoice'
        supply:
          $ref: '#/components/schemas/AdminOverviewSupply'
      required: [funnel, voice, supply]
    AdminOverviewPointsWeekly:
      type: object
      properties:
        week:
          type: string
        issued:
          type: number
        redeemed:
          type: number
      required: [week, issued, redeemed]
    AdminOverviewPointsSystem:
      type: object
      properties:
        system:
          type: string
          enum: [CAMPAIGN, COLLECTION]
        issued:
          type: number
        redeemed:
          type: number
        reversed:
          type: number
        outstanding:
          type: number
        weekly:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewPointsWeekly'
      required: [system, issued, redeemed, reversed, outstanding, weekly]
    AdminOverviewRedemptionProduct:
      type: object
      properties:
        productName:
          type: string
        count:
          type: number
        remainingQuantity:
          type: number
          nullable: true
      required: [productName, count, remainingQuantity]
    AdminOverviewRedemptions:
      type: object
      properties:
        total:
          type: number
        pending:
          type: number
        pendingStale:
          type: number
        topProducts:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewRedemptionProduct'
      required: [total, pending, pendingStale, topProducts]
    AdminOverviewPoints:
      type: object
      properties:
        systems:
          type: array
          items:
            $ref: '#/components/schemas/AdminOverviewPointsSystem'
        redemptions:
          $ref: '#/components/schemas/AdminOverviewRedemptions'
      required: [systems, redemptions]
    AdminPlatformMonthlyTarget:
      type: object
      properties:
        currency:
          type: string
        targetCents:
          type: number
      required: [currency, targetCents]
    AdminPlatformOverviewSettings:
      type: object
      properties:
        timezone:
          type: string
        monthlyTargets:
          type: array
          items:
            $ref: '#/components/schemas/AdminPlatformMonthlyTarget'
        updatedAt:
          type: string
          nullable: true
      required: [timezone, monthlyTargets, updatedAt]
    AdminPlatformMonthlyTargetPatch:
      type: object
      properties:
        currency:
          type: string
          minLength: 3
          maxLength: 3
          pattern: '^(?:[Uu][Ss][Dd]|[Hh][Kk][Dd]|[Cc][Nn][Yy]|[Jj][Pp][Yy]|[Kk][Rr][Ww]|[Tt][Ww][Dd])$'
          description: Supported currency code; normalized to lowercase.
        targetCents:
          type: integer
          minimum: 0
          nullable: true
      required: [currency, targetCents]
      additionalProperties: false
    AdminPlatformOverviewSettingsPatch:
      type: object
      properties:
        timezone:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[^\r\n]*\S[^\r\n]*$'
          nullable: true
        monthlyTargets:
          type: array
          maxItems: 6
          items:
            $ref: '#/components/schemas/AdminPlatformMonthlyTargetPatch'
      additionalProperties: false
    AdminOverviewPulseApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewPulse'
      required:
        - status
        - data
    AdminOverviewAlertListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewAlertList'
      required:
        - status
        - data
    AdminOverviewAlertAckResultApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewAlertAckResult'
      required:
        - status
        - data
    AdminOverviewStripeFeeRetry:
      type: object
      properties:
        scanned:
          type: integer
        included:
          type: integer
        excluded:
          type: integer
        failed:
          type: integer
        skipped:
          type: integer
      required: [scanned, included, excluded, failed, skipped]
    AdminOverviewStripeFeeRetryApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewStripeFeeRetry'
      required: [status, data]
    AdminOverviewWithdrawalRetry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [PROCESSING]
      required: [id, status]
    AdminOverviewWithdrawalRetryApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewWithdrawalRetry'
      required: [status, data]
    AdminOverviewFulfillmentApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewFulfillment'
      required:
        - status
        - data
    AdminOverviewCalendarApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewCalendar'
      required:
        - status
        - data
    AdminOverviewMarketApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewMarket'
      required:
        - status
        - data
    AdminOverviewCommunityApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewCommunity'
      required:
        - status
        - data
    AdminOverviewPointsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOverviewPoints'
      required:
        - status
        - data
    AdminPlatformOverviewSettingsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminPlatformOverviewSettings'
      required:
        - status
        - data
    ApiErrorResponse:
      type: object
      properties:
        status:
          type: string
          enum: [error]
        data:
          type: object
          properties:
            message:
              type: string
            code:
              type: string
          required:
            - message
      required:
        - status
        - data
    CampaignCollectionAdminErrorResponse:
      type: object
      properties:
        status:
          type: string
          enum: [error]
        data:
          type: object
          properties:
            message:
              type: string
            code:
              type: string
              enum:
                [ARCHIVED_BINDING, CHILD_OCCUPIED, CONFIRMATION_MISMATCH, DELETE_BLOCKED, SCOPE_IMMUTABLE, STALE_WRITE]
            blockingCounts:
              allOf:
                - $ref: '#/components/schemas/CampaignCollectionBlockingCounts'
              nullable: true
          required:
            - message
            - code
            - blockingCounts
      required:
        - status
        - data
    CampaignCollectionStatus:
      type: string
      enum: [DRAFT, ACTIVE, ARCHIVED]
    CampaignCollectionBlockingCounts:
      type: object
      properties:
        campaigns:
          type: integer
          minimum: 0
        marketplaceCollections:
          type: integer
          minimum: 0
        pointAccounts:
          type: integer
          minimum: 0
        ledgerEntries:
          type: integer
          minimum: 0
        importBatches:
          type: integer
          minimum: 0
        checkoutSnapshotItems:
          type: integer
          minimum: 0
        redemptions:
          type: integer
          minimum: 0
      required:
        - campaigns
        - marketplaceCollections
        - pointAccounts
        - ledgerEntries
        - importBatches
        - checkoutSnapshotItems
        - redemptions
    CampaignCollectionMetadata:
      type: object
      properties:
        id:
          type: string
          format: uuid
        ownerId:
          type: string
        name:
          type: string
          maxLength: 120
        description:
          type: string
          nullable: true
          maxLength: 2000
        tags:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 40
        company:
          type: string
          nullable: true
          maxLength: 120
        website:
          type: string
          format: uri
          nullable: true
        logoUrl:
          type: string
          format: uri
          nullable: true
        bannerUrl:
          type: string
          format: uri
          nullable: true
        twitterId:
          type: string
          nullable: true
          maxLength: 120
        facebookId:
          type: string
          nullable: true
          maxLength: 120
        instagramId:
          type: string
          nullable: true
          maxLength: 120
        threadId:
          type: string
          nullable: true
          maxLength: 120
        tiktokId:
          type: string
          nullable: true
          maxLength: 120
        status:
          $ref: '#/components/schemas/CampaignCollectionStatus'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - ownerId
        - name
        - description
        - tags
        - company
        - website
        - logoUrl
        - bannerUrl
        - twitterId
        - facebookId
        - instagramId
        - threadId
        - tiktokId
        - status
        - createdAt
        - updatedAt
    CampaignCollectionCampaignRef:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        status:
          type: string
      required:
        - id
        - title
        - status
    CampaignCollectionMarketplaceRef:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
      required:
        - id
        - name
        - slug
    AdminCampaignCollectionListItem:
      allOf:
        - $ref: '#/components/schemas/CampaignCollectionMetadata'
        - type: object
          properties:
            campaignCount:
              type: integer
              minimum: 0
            marketplaceCollectionCount:
              type: integer
              minimum: 0
            hasPointsActivity:
              type: boolean
          required:
            - campaignCount
            - marketplaceCollectionCount
            - hasPointsActivity
    AdminCampaignCollectionDetail:
      allOf:
        - $ref: '#/components/schemas/CampaignCollectionMetadata'
        - type: object
          properties:
            campaigns:
              type: array
              items:
                $ref: '#/components/schemas/CampaignCollectionCampaignRef'
            marketplaceCollections:
              type: array
              items:
                $ref: '#/components/schemas/CampaignCollectionMarketplaceRef'
            blockingCounts:
              $ref: '#/components/schemas/CampaignCollectionBlockingCounts'
            mutable:
              type: boolean
            deletable:
              type: boolean
          required:
            - campaigns
            - marketplaceCollections
            - blockingCounts
            - mutable
            - deletable
    CampaignCollectionCreateBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type: string
          nullable: true
          maxLength: 2000
        tags:
          type: array
          maxItems: 20
          items:
            type: string
            minLength: 1
            maxLength: 40
        company:
          type: string
          nullable: true
          maxLength: 120
        website:
          type: string
          format: uri
          nullable: true
        logoUrl:
          type: string
          format: uri
          nullable: true
        bannerUrl:
          type: string
          format: uri
          nullable: true
        twitterId:
          type: string
          nullable: true
          maxLength: 120
        facebookId:
          type: string
          nullable: true
          maxLength: 120
        instagramId:
          type: string
          nullable: true
          maxLength: 120
        threadId:
          type: string
          nullable: true
          maxLength: 120
        tiktokId:
          type: string
          nullable: true
          maxLength: 120
        status:
          $ref: '#/components/schemas/CampaignCollectionStatus'
        campaignIds:
          type: array
          maxItems: 500
          uniqueItems: true
          items:
            type: string
            format: uuid
        marketplaceCollectionIds:
          type: array
          maxItems: 500
          uniqueItems: true
          items:
            type: string
            format: uuid
      required:
        - name
    CampaignCollectionPatchBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type: string
          nullable: true
          maxLength: 2000
        tags:
          type: array
          maxItems: 20
          items:
            type: string
            minLength: 1
            maxLength: 40
        company:
          type: string
          nullable: true
          maxLength: 120
        website:
          type: string
          format: uri
          nullable: true
        logoUrl:
          type: string
          format: uri
          nullable: true
        bannerUrl:
          type: string
          format: uri
          nullable: true
        twitterId:
          type: string
          nullable: true
          maxLength: 120
        facebookId:
          type: string
          nullable: true
          maxLength: 120
        instagramId:
          type: string
          nullable: true
          maxLength: 120
        threadId:
          type: string
          nullable: true
          maxLength: 120
        tiktokId:
          type: string
          nullable: true
          maxLength: 120
        status:
          $ref: '#/components/schemas/CampaignCollectionStatus'
        campaignIds:
          type: array
          maxItems: 500
          uniqueItems: true
          items:
            type: string
            format: uuid
        marketplaceCollectionIds:
          type: array
          maxItems: 500
          uniqueItems: true
          items:
            type: string
            format: uuid
        expectedUpdatedAt:
          type: string
          format: date-time
      required:
        - expectedUpdatedAt
    CampaignCollectionDeleteBody:
      type: object
      properties:
        expectedUpdatedAt:
          type: string
          format: date-time
        confirmName:
          type: string
          minLength: 1
          maxLength: 120
      required:
        - expectedUpdatedAt
        - confirmName
    AdminCampaignCollectionListResult:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AdminCampaignCollectionListItem'
        total:
          type: integer
          minimum: 0
        page:
          type: integer
          minimum: 1
        limit:
          type: integer
          minimum: 10
        totalPages:
          type: integer
          minimum: 1
      required:
        - items
        - total
        - page
        - limit
        - totalPages
    AdminCampaignCollectionListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminCampaignCollectionListResult'
      required:
        - status
        - data
    AdminCampaignCollectionDetailApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminCampaignCollectionDetail'
      required:
        - status
        - data
    CampaignCollectionDeleteApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
          required:
            - id
      required:
        - status
        - data
    AdminCampaignListItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        collectionId:
          type: string
          format: uuid
          nullable: true
        collectionName:
          type: string
          nullable: true
        title:
          type: string
        slug:
          type: string
        status:
          type: string
        language:
          type: string
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time
        creatorCount:
          type: integer
        totalPointsPool:
          type: integer
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - collectionId
        - collectionName
        - title
        - slug
        - status
        - language
        - startDate
        - endDate
        - creatorCount
        - totalPointsPool
        - createdAt
    AdminCampaignListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/AdminCampaignListItem'
      required:
        - status
        - data
    AdminCampaignMutationApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminCampaignMutationResult'
      required:
        - status
        - data
    AdminCampaignMutationResult:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
      required:
        - id
        - slug
    AdminCampaignAttachment:
      type: object
      properties:
        url:
          type: string
          format: uri
        type:
          type: string
          enum: [image, video]
      required:
        - url
        - type
    AdminCampaignPoster:
      type: object
      properties:
        url:
          type: string
          format: uri
        type:
          type: string
          enum: [image]
      required:
        - url
        - type
    AdminCampaignInfoInput:
      type: object
      properties:
        description:
          type: string
        poster:
          allOf:
            - $ref: '#/components/schemas/AdminCampaignPoster'
          nullable: true
        operatingHours:
          type: string
        location:
          type: string
        ticketPrice:
          type: string
        subtitle:
          type: string
          maxLength: 120
      required:
        - description
        - operatingHours
        - location
        - ticketPrice
    AdminCampaignParticipationMethodInput:
      type: object
      properties:
        description:
          type: string
        poster:
          allOf:
            - $ref: '#/components/schemas/AdminCampaignPoster'
          nullable: true
        notes:
          type: string
        steps:
          type: array
          items:
            $ref: '#/components/schemas/CampaignParticipationStep'
        keyNotes:
          type: array
          items:
            type: string
      required:
        - description
    AdminCampaignScoreDistributionEntry:
      type: object
      properties:
        rank:
          type: integer
          minimum: 1
        points:
          type: integer
          minimum: 0
      required:
        - rank
        - points
    AdminCampaignRedirectLink:
      type: object
      properties:
        label:
          type: string
        url:
          type: string
          format: uri
        tracking:
          type: string
      required:
        - label
        - url
    AdminCampaignMutationBody:
      type: object
      properties:
        title:
          type: string
          maxLength: 200
        slug:
          type: string
          maxLength: 200
        info:
          $ref: '#/components/schemas/AdminCampaignInfoInput'
        participationMethod:
          $ref: '#/components/schemas/AdminCampaignParticipationMethodInput'
        rewards:
          type: array
          items:
            type: string
        collectionId:
          type: string
          format: uuid
        hashtag:
          type: string
          maxLength: 100
        hashtags:
          type: array
          maxItems: 3
          items:
            type: string
            maxLength: 100
        keywords:
          type: array
          items:
            type: string
        redirectLinks:
          type: array
          items:
            $ref: '#/components/schemas/AdminCampaignRedirectLink'
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/AdminCampaignAttachment'
        imageCount:
          type: integer
          minimum: 0
          maximum: 50
        videoCount:
          type: integer
          minimum: 0
          maximum: 50
        totalPointsPool:
          type: integer
          minimum: 1
        participationRewardPoints:
          type: integer
          minimum: 1
          default: 100
        rankMode:
          type: string
          enum: [TOP_PERFORMERS, ENGAGEMENT_BASED]
        scoreDistribution:
          type: array
          items:
            $ref: '#/components/schemas/AdminCampaignScoreDistributionEntry'
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time
        status:
          type: string
          enum: [DRAFT, ACTIVE, PAUSED, COMPLETED, CANCELLED]
        language:
          type: string
      required:
        - title
        - slug
        - info
        - participationMethod
        - totalPointsPool
        - rankMode
        - scoreDistribution
        - startDate
        - endDate
        - status
        - language
    CampaignListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CampaignListData'
      required:
        - status
        - data
    CampaignParticipationSubmissionRequest:
      type: object
      description: Signed-out requests provide an email to receive a hashed six-digit sign-in OTP. URLs remain client-side until the authenticated request.
      properties:
        threadsPostUrl:
          type: string
          format: uri
          nullable: true
        vocusPostUrl:
          type: string
          format: uri
          nullable: true
        email:
          type: string
          format: email
          nullable: true
      anyOf:
        - required: [threadsPostUrl]
        - required: [vocusPostUrl]
    CampaignSubmissionData:
      oneOf:
        - $ref: '#/components/schemas/CampaignSubmissionPendingOtpData'
        - $ref: '#/components/schemas/CampaignSubmissionCompletedData'
      discriminator:
        propertyName: outcome
        mapping:
          PENDING_OTP: '#/components/schemas/CampaignSubmissionPendingOtpData'
          COMPLETED: '#/components/schemas/CampaignSubmissionCompletedData'
    CampaignSubmissionPendingOtpData:
      type: object
      additionalProperties: false
      properties:
        outcome:
          type: string
          enum: [PENDING_OTP]
        confirmationSent:
          type: boolean
      required: [outcome, confirmationSent]
    CampaignSubmissionCompletedData:
      type: object
      additionalProperties: false
      properties:
        outcome:
          type: string
          enum: [COMPLETED]
        participationId:
          type: string
          format: uuid
        pointsAwarded:
          type: integer
        awardedAt:
          type: string
          format: date-time
        idempotent:
          type: boolean
      required: [outcome, participationId, pointsAwarded, awardedAt, idempotent]
    CampaignSubmissionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CampaignSubmissionData'
      required: [status, data]
    CampaignSubmissionReviewRequest:
      type: object
      properties:
        ids:
          type: array
          minItems: 1
          maxItems: 500
          items: { type: string, format: uuid }
        status:
          type: string
          enum: [APPROVED, REJECTED]
        reason:
          type: string
          nullable: true
      required: [ids, status]
    CampaignSubmissionAwardRequest:
      type: object
      properties:
        ids:
          type: array
          minItems: 1
          maxItems: 500
          items: { type: string, format: uuid }
      required: [ids]
    CampaignSubmissionMutationApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            updated: { type: integer }
          required: [updated]
      required: [status, data]
    CampaignSubmissionAwardApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            awarded: { type: integer }
            skipped: { type: integer }
          required: [awarded, skipped]
      required: [status, data]
    CampaignSubmissionListApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/CampaignSubmissionItem'
            total: { type: integer }
            limit: { type: integer }
            offset: { type: integer }
          required: [items, total, limit, offset]
      required: [status, data]
    CampaignSubmissionItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        campaignId: { type: string, format: uuid }
        campaignTitle: { type: string }
        campaignSlug: { type: string }
        userId: { type: string }
        email: { type: string, format: email }
        name: { type: string }
        threadsPostUrl: { type: string, format: uri, nullable: true }
        vocusPostUrl: { type: string, format: uri, nullable: true }
        postSubmittedAt: { type: string, format: date-time, nullable: true }
        postReviewStatus: { type: string, enum: [PENDING, APPROVED, REJECTED], nullable: true }
        postReviewReason: { type: string, nullable: true }
        postReviewedAt: { type: string, format: date-time, nullable: true }
        postReviewedByUserId: { type: string, nullable: true }
        participationRewardPoints: { type: integer, nullable: true }
        participationRewardAwardedAt: { type: string, format: date-time, nullable: true }
        participationRewardAwardedByUserId: { type: string, nullable: true }
      required: [id, campaignId, campaignTitle, campaignSlug, userId, email, name]
    CampaignParticipationGoogleIdentityRequest:
      type: object
      properties:
        reservationId:
          type: string
          format: uuid
        locale:
          type: string
          enum: [en, ja, ko, zh-TW, zh-CN]
      required:
        - reservationId
    CampaignParticipationConfirmationRequest:
      type: object
      properties:
        confirmationToken:
          type: string
      required:
        - confirmationToken
    CampaignParticipationReservationData:
      type: object
      properties:
        reservationId:
          type: string
          format: uuid
        expiresAt:
          type: string
          format: date-time
        outcome:
          type: string
          enum: [CONFIRMED, PENDING_EMAIL, GOOGLE_IDENTITY_REQUIRED]
        pointsAwarded:
          type: integer
          enum: [0, 100]
        requiresGoogleIdentity:
          type: boolean
        confirmationSent:
          type: boolean
        idempotent:
          type: boolean
      required:
        - reservationId
        - expiresAt
        - outcome
        - pointsAwarded
        - requiresGoogleIdentity
        - confirmationSent
        - idempotent
    CampaignParticipationGoogleIdentityData:
      type: object
      properties:
        reservationId:
          type: string
          format: uuid
        expiresAt:
          type: string
          format: date-time
        confirmationSent:
          type: boolean
        idempotent:
          type: boolean
      required:
        - reservationId
        - expiresAt
        - confirmationSent
        - idempotent
    CampaignParticipationConfirmationData:
      type: object
      properties:
        reservationId:
          type: string
          format: uuid
        pointsAwarded:
          type: integer
          enum: [100]
        confirmed:
          type: boolean
        idempotent:
          type: boolean
      required:
        - reservationId
        - pointsAwarded
        - confirmed
        - idempotent
    CampaignParticipationReservationApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CampaignParticipationReservationData'
      required:
        - status
        - data
    CampaignParticipationGoogleIdentityApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CampaignParticipationGoogleIdentityData'
      required:
        - status
        - data
    CampaignParticipationConfirmationApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CampaignParticipationConfirmationData'
      required:
        - status
        - data
    CampaignDetailApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CampaignDetail'
      required:
        - status
        - data
    SettingsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/CollectionWithStats'
      required:
        - status
        - data
    OverviewApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/OverviewData'
      required:
        - status
        - data
    ContributionDetailApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/ContributionDetail'
      required:
        - status
        - data
    StoreProductApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/StoreProduct'
      required:
        - status
        - data
    AdminStoreProduct:
      allOf:
        - $ref: '#/components/schemas/StoreProduct'
        - type: object
          properties:
            digitalDeliveryMode:
              type: string
              enum: [NONE, ATTACHMENTS]
            tags:
              type: array
              maxItems: 20
              items:
                type: string
                maxLength: 48
                pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
            earlyBirdPriceCents:
              type: integer
              nullable: true
            earlyBirdStartsAt:
              type: string
              format: date-time
              nullable: true
            earlyBirdEndsAt:
              type: string
              format: date-time
              nullable: true
          required:
            - digitalDeliveryMode
            - tags
    AdminStoreProductApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminStoreProduct'
      required:
        - status
        - data
    AdminStoreProductTagsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 48
            pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
      required:
        - status
        - data
    StoreProductListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/StoreProduct'
      required:
        - status
        - data
    StoreProductPaginatedListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/StoreProduct'
        pagination:
          $ref: '#/components/schemas/PaginationMeta'
      required:
        - status
        - data
        - pagination
    AdminStoreProductListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/StoreProduct'
        pagination:
          $ref: '#/components/schemas/PaginationMeta'
      required:
        - status
        - data
        - pagination
    StoreReorderBody:
      type: object
      properties:
        collectionId:
          type: string
          format: uuid
          nullable: true
          description: Collection whose product order is being replaced. Null scopes the snapshot to products without a collection; omitted preserves legacy global ordering behavior.
        orderedIds:
          type: array
          minItems: 1
          maxItems: 1000
          uniqueItems: true
          items:
            type: string
            format: uuid
          description: Complete curated product order for the selected collection, top-first.
      required:
        - orderedIds
    StoreReorderApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            updated:
              type: integer
              minimum: 0
          required:
            - updated
      required:
        - status
        - data
    CreatorProductStatus:
      type: string
      enum: [DRAFT, PENDING_REVIEW, ACTIVE, INACTIVE, REJECTED]
    CreatorNftHolding:
      type: object
      properties:
        tokenDataId:
          type: string
        name:
          type: string
          nullable: true
        imageUrl:
          type: string
          nullable: true
        tokenUri:
          type: string
          nullable: true
        collectionId:
          type: string
        collectionName:
          type: string
          nullable: true
      required:
        - tokenDataId
        - name
        - imageUrl
        - tokenUri
        - collectionId
        - collectionName
    CreatorEligibilityData:
      type: object
      properties:
        status:
          type: string
          enum: [needs_wallet, not_holder, eligible]
        walletAddress:
          type: string
          nullable: true
        collectionId:
          type: string
          nullable: true
        collectionName:
          type: string
          nullable: true
        verifiedAt:
          type: string
          format: date-time
          nullable: true
        holdings:
          type: array
          items:
            $ref: '#/components/schemas/CreatorNftHolding'
      required:
        - status
        - walletAddress
    CreatorEligibilityApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatorEligibilityData'
      required:
        - status
        - data
    CreatorFeeConfig:
      type: object
      properties:
        copyrightFeeBps:
          type: integer
          minimum: 0
          maximum: 10000
        storeFeeBps:
          type: integer
          minimum: 0
          maximum: 10000
        withdrawalFeeBps:
          type: integer
          minimum: 0
          maximum: 10000
      required:
        - copyrightFeeBps
        - storeFeeBps
        - withdrawalFeeBps
    CreatorFeeConfigPatchBody:
      type: object
      properties:
        copyrightFeeBps:
          type: integer
          minimum: 0
          maximum: 10000
        storeFeeBps:
          type: integer
          minimum: 0
          maximum: 10000
        withdrawalFeeBps:
          type: integer
          minimum: 0
          maximum: 10000
      required:
        - copyrightFeeBps
        - storeFeeBps
        - withdrawalFeeBps
    CreatorFeeConfigApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatorFeeConfig'
      required:
        - status
        - data
    RefundFeeConfig:
      type: object
      properties:
        refundFeeBps:
          type: integer
          minimum: 0
          maximum: 9999
      required:
        - refundFeeBps
    RefundFeeConfigPatchBody:
      type: object
      properties:
        refundFeeBps:
          type: integer
          minimum: 0
          maximum: 9999
      required:
        - refundFeeBps
    RefundFeeConfigApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/RefundFeeConfig'
      required:
        - status
        - data
    CreatorProduct:
      type: object
      properties:
        id:
          type: string
          format: uuid
        collectionId:
          type: string
          format: uuid
          nullable: true
        creatorUserId:
          type: string
          nullable: true
        name:
          type: string
        description:
          type: string
          nullable: true
        seoTitle:
          type: string
          nullable: true
        seoDescription:
          type: string
          nullable: true
        handle:
          type: string
          nullable: true
          maxLength: 200
        imageUrl:
          type: string
          nullable: true
        imageUrls:
          type: array
          items:
            type: string
        meta:
          $ref: '#/components/schemas/StoreProductMeta'
        optionGroups:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductOptionGroup'
        variants:
          type: array
          items:
            $ref: '#/components/schemas/CreatorProductVariant'
        pointsCost:
          type: integer
          nullable: true
        priceCents:
          type: integer
          nullable: true
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        totalQuantity:
          type: integer
          nullable: true
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
        status:
          $ref: '#/components/schemas/CreatorProductStatus'
        submittedAt:
          type: string
          format: date-time
          nullable: true
        approvedAt:
          type: string
          format: date-time
          nullable: true
        approvedByUserId:
          type: string
          nullable: true
        rejectedAt:
          type: string
          format: date-time
          nullable: true
        rejectedByUserId:
          type: string
          nullable: true
        rejectionReason:
          type: string
          nullable: true
        salesCents:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - creatorUserId
        - name
        - handle
        - meta
        - currency
        - category
        - status
        - createdAt
        - updatedAt
    CreatorProductVariant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        options:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariantOption'
        imageUrl:
          type: string
          format: uri
          nullable: true
        pointsCost:
          type: integer
          nullable: true
        priceCents:
          type: integer
          nullable: true
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        stripePriceId:
          type: string
          nullable: true
        totalQuantity:
          type: integer
          nullable: true
        inventoryMode:
          type: string
          enum: [INHERIT, OVERRIDE]
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        availableQuantity:
          type: integer
          nullable: true
      required:
        - id
        - name
        - options
        - imageUrl
        - currency
        - status
    CreatorProductApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatorProduct'
      required:
        - status
        - data
    CreatorProductListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/CreatorProduct'
      required:
        - status
        - data
    CreatorProductCreateBody:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        imageUrl:
          type: string
          nullable: true
        imageUrls:
          type: array
          nullable: true
          items:
            type: string
            format: uri
        meta:
          $ref: '#/components/schemas/StoreProductMeta'
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          default: OTHER
        optionGroups:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductOptionGroupInput'
        variants:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/CreatorProductVariantInput'
      required:
        - name
        - variants
    CustomDigitalCreatorProductCreateBody:
      type: object
      properties:
        creationIntent:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 120
        description:
          type: string
          maxLength: 1000
          nullable: true
        priceCents:
          type: integer
          minimum: 1
        currency:
          type: string
          enum: [usd, hkd, jpy, twd]
      required:
        - creationIntent
        - name
        - priceCents
        - currency
    AdminMediaUploadBody:
      type: object
      properties:
        filename:
          type: string
          minLength: 1
        contentType:
          type: string
          minLength: 1
        size:
          type: integer
          minimum: 1
        scope:
          type: string
          enum: [campaign, store]
          default: campaign
        productId:
          type: string
          format: uuid
          description: Required only for a creator-owned charged custom digital-product cover upload.
      required:
        - filename
        - contentType
        - size
    AdminMediaUploadApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            uploadUrl:
              type: string
              format: uri
            token:
              type: string
            path:
              type: string
            publicUrl:
              type: string
              format: uri
            mediaType:
              type: string
              enum: [image, video]
          required: [uploadUrl, token, path, publicUrl, mediaType]
      required: [status, data]
    CreatorProductPatchBody:
      allOf:
        - $ref: '#/components/schemas/CreatorProductCreateBody'
    CreatorProductVariantInput:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        options:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariantOptionInput'
        pointsCost:
          type: integer
          nullable: true
          description: Creator products must not use pointsCost.
        priceCents:
          type: integer
          nullable: true
          description: Creator product price in minor currency units.
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          description: Legacy CNY/KRW values remain accepted only when preserving an existing product currency.
          default: usd
        stripeProductId:
          type: string
          nullable: true
        stripePriceId:
          type: string
          nullable: true
        totalQuantity:
          type: integer
          nullable: true
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
          default: ACTIVE
      required:
        - name
        - options
    CreatorStripeAccount:
      type: object
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
        stripeAccountId:
          type: string
        chargesEnabled:
          type: boolean
        payoutsEnabled:
          type: boolean
        transfersEnabled:
          type: boolean
        detailsSubmitted:
          type: boolean
        requirementsCurrentlyDue:
          type: array
          items:
            type: string
        requirementsPastDue:
          type: array
          items:
            type: string
        disabledReason:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        defaultCurrency:
          type: string
          nullable: true
        lastSyncedAt:
          type: string
          format: date-time
          nullable: true
        withdrawalsEnabled:
          type: boolean
      required:
        - id
        - userId
        - stripeAccountId
        - chargesEnabled
        - payoutsEnabled
        - transfersEnabled
        - detailsSubmitted
        - requirementsCurrentlyDue
        - requirementsPastDue
        - withdrawalsEnabled
    CreatorStripeAccountApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CreatorStripeAccount'
      required:
        - status
        - data
    CreatorStripeOnboardingLinkBody:
      type: object
      properties:
        locale:
          type: string
    CreatorStripeOnboardingLink:
      type: object
      properties:
        url:
          type: string
          format: uri
      required:
        - url
    CreatorStripeOnboardingLinkApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatorStripeOnboardingLink'
      required:
        - status
        - data
    CreatorWithdrawal:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [PENDING, PROCESSING, PAID, FAILED, CANCELED]
        currency:
          type: string
        grossAmountCents:
          type: integer
        withdrawalFeeBps:
          type: integer
        withdrawalFeeCents:
          type: integer
        netAmountCents:
          type: integer
        failureCode:
          type: string
          nullable: true
        failureMessage:
          type: string
          nullable: true
        requestedAt:
          type: string
          format: date-time
        processedAt:
          type: string
          format: date-time
          nullable: true
        paidAt:
          type: string
          format: date-time
          nullable: true
        failedAt:
          type: string
          format: date-time
          nullable: true
      required:
        - id
        - status
        - currency
        - grossAmountCents
        - withdrawalFeeBps
        - withdrawalFeeCents
        - netAmountCents
        - requestedAt
    CreatorWithdrawalCreateBody:
      type: object
      properties:
        amountCents:
          type: integer
          minimum: 1
        currency:
          type: string
      required:
        - currency
    CreatorWithdrawalListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/CreatorWithdrawal'
      required:
        - status
        - data
    CreatorWithdrawalCreateData:
      type: object
      properties:
        withdrawal:
          $ref: '#/components/schemas/CreatorWithdrawal'
      required:
        - withdrawal
    CreatorWithdrawalCreateApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatorWithdrawalCreateData'
      required:
        - status
        - data
    CreatorDashboardSummary:
      type: object
      properties:
        currency:
          type: string
        todaySalesCents:
          type: integer
        balanceCents:
          type: integer
        earnedBalanceCents:
          type: integer
        settledBalanceCents:
          type: integer
        pendingBalanceCents:
          type: integer
        pendingWithdrawalCents:
          type: integer
        paidWithdrawalCents:
          type: integer
        availableBalanceCents:
          type: integer
        estimatedWithdrawalFeeCents:
          type: integer
        estimatedWithdrawableCents:
          type: integer
      required:
        - currency
        - todaySalesCents
        - balanceCents
        - earnedBalanceCents
        - settledBalanceCents
        - pendingBalanceCents
        - pendingWithdrawalCents
        - paidWithdrawalCents
        - availableBalanceCents
        - estimatedWithdrawalFeeCents
        - estimatedWithdrawableCents
    CreatorBarChartBucket:
      type: object
      properties:
        label:
          type: string
        amountCents:
          type: integer
        currency:
          type: string
      required:
        - label
        - amountCents
        - currency
    CreatorDashboardData:
      type: object
      properties:
        eligibility:
          $ref: '#/components/schemas/CreatorEligibilityData'
        guidelineAcceptance:
          $ref: '#/components/schemas/CreatorGuidelineAcceptanceData'
        summary:
          $ref: '#/components/schemas/CreatorDashboardSummary'
        charts:
          type: object
          properties:
            sevenDays:
              type: array
              items:
                $ref: '#/components/schemas/CreatorBarChartBucket'
            thirtyDays:
              type: array
              items:
                $ref: '#/components/schemas/CreatorBarChartBucket'
            year:
              type: array
              items:
                $ref: '#/components/schemas/CreatorBarChartBucket'
          required:
            - sevenDays
            - thirtyDays
            - year
        fees:
          $ref: '#/components/schemas/CreatorFeeConfig'
        products:
          type: array
          items:
            $ref: '#/components/schemas/CreatorProduct'
        stripeAccount:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CreatorStripeAccount'
        withdrawals:
          type: array
          items:
            $ref: '#/components/schemas/CreatorWithdrawal'
      required:
        - eligibility
        - guidelineAcceptance
        - summary
        - charts
        - fees
        - products
        - stripeAccount
        - withdrawals
    CreatorDashboardApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatorDashboardData'
      required:
        - status
        - data
    AdminCreatorProductReviewBody:
      type: object
      properties:
        reason:
          type: string
      required:
        - reason
    AdminStripeCollectionEarningsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminStripeCollectionEarnings'
      required:
        - status
        - data
    AdminStripeCollectionEarnings:
      type: object
      properties:
        collections:
          type: array
          items:
            $ref: '#/components/schemas/AdminStripeCollectionEarningsSummary'
        payments:
          type: array
          items:
            $ref: '#/components/schemas/AdminStripeCollectionEarningsPayment'
        globalMetrics:
          $ref: '#/components/schemas/AdminStripeCollectionEarningsGlobalMetrics'
        pagination:
          $ref: '#/components/schemas/PaginationMeta'
      required:
        - collections
        - payments
        - globalMetrics
        - pagination
    AdminStripeCollectionEarningsGlobalMetrics:
      type: object
      properties:
        successfulPayments:
          type: integer
          minimum: 0
        syncedPayments:
          type: integer
          minimum: 0
      required:
        - successfulPayments
        - syncedPayments
    AdminStripeCollectionEarningsSummary:
      type: object
      properties:
        collectionId:
          type: string
          format: uuid
        collectionName:
          type: string
        currency:
          type: string
        paymentCount:
          type: integer
          minimum: 0
        amount:
          type: string
          pattern: '^-?[0-9]+$'
        fee:
          type: string
          pattern: '^-?[0-9]+$'
        net:
          type: string
          pattern: '^-?[0-9]+$'
        lastSyncedAt:
          type: string
          format: date-time
      required:
        - collectionId
        - collectionName
        - currency
        - paymentCount
        - amount
        - fee
        - net
        - lastSyncedAt
    AdminStripeCollectionEarningsPayment:
      type: object
      properties:
        stripeSessionId:
          type: string
        stripePaymentIntentId:
          type: string
        stripeChargeId:
          type: string
          nullable: true
        stripeBalanceTransactionId:
          type: string
          nullable: true
        collectionId:
          type: string
          format: uuid
        collectionName:
          type: string
        currency:
          type: string
        amount:
          type: string
          pattern: '^-?[0-9]+$'
        fee:
          type: string
          pattern: '^-?[0-9]+$'
        net:
          type: string
          pattern: '^-?[0-9]+$'
        stripeCreatedAt:
          type: string
          format: date-time
          nullable: true
        lastCheckedAt:
          type: string
          format: date-time
      required:
        - stripeSessionId
        - stripePaymentIntentId
        - stripeChargeId
        - stripeBalanceTransactionId
        - collectionId
        - collectionName
        - currency
        - amount
        - fee
        - net
        - stripeCreatedAt
        - lastCheckedAt
    StripeCollectionEarningsSyncApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/StripeCollectionEarningsSyncSummary'
      required:
        - status
        - data
    StripeCollectionEarningsSyncSummary:
      type: object
      properties:
        scanned:
          type: integer
          minimum: 0
        included:
          type: integer
          minimum: 0
        excluded:
          type: integer
          minimum: 0
        failed:
          type: integer
          minimum: 0
        skipped:
          type: integer
          minimum: 0
      required:
        - scanned
        - included
        - excluded
        - failed
        - skipped
    CronErrorResponse:
      type: object
      properties:
        error:
          type: string
      required:
        - error
    AdminAnalyticsCollectionAmount:
      type: object
      properties:
        currency:
          type: string
        revenueCents:
          type: integer
      required:
        - currency
        - revenueCents
    AdminAnalyticsCollectionSummary:
      type: object
      properties:
        collectionId:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        coverImageUrl:
          type: string
          nullable: true
        isTicketCollection:
          type: boolean
        paidOrderCount:
          type: integer
        paidItemQuantity:
          type: integer
        firstPaidAt:
          type: string
          format: date-time
          nullable: true
        lastPaidAt:
          type: string
          format: date-time
          nullable: true
        amountsByCurrency:
          type: array
          items:
            $ref: '#/components/schemas/AdminAnalyticsCollectionAmount'
        recent7dAmountsByCurrency:
          type: array
          items:
            $ref: '#/components/schemas/AdminAnalyticsCollectionAmount'
        recent7dItemQuantity:
          type: integer
        recent30dAmountsByCurrency:
          type: array
          items:
            $ref: '#/components/schemas/AdminAnalyticsCollectionAmount'
        recent30dItemQuantity:
          type: integer
        monthAmountsByCurrency:
          type: array
          items:
            $ref: '#/components/schemas/AdminAnalyticsCollectionAmount'
        prior7dItemQuantity:
          type: integer
      required:
        - collectionId
        - name
        - slug
        - coverImageUrl
        - isTicketCollection
        - paidOrderCount
        - paidItemQuantity
        - firstPaidAt
        - lastPaidAt
        - amountsByCurrency
        - recent7dAmountsByCurrency
        - recent7dItemQuantity
        - recent30dAmountsByCurrency
        - recent30dItemQuantity
        - monthAmountsByCurrency
        - prior7dItemQuantity
    AdminAnalyticsCollectionList:
      type: object
      properties:
        collections:
          type: array
          items:
            $ref: '#/components/schemas/AdminAnalyticsCollectionSummary'
      required:
        - collections
    AdminAnalyticsCollectionListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminAnalyticsCollectionList'
      required:
        - status
        - data
    AdminCollectionAnalyticsSettings:
      type: object
      properties:
        boardType:
          type: string
          enum: [TICKET, PRODUCT]
          nullable: true
        targetUnits:
          type: integer
          nullable: true
        breakEvenUnits:
          type: integer
          nullable: true
        costBasisPoints:
          type: integer
          nullable: true
        feeBasisPoints:
          type: integer
          nullable: true
        periodStart:
          type: string
          nullable: true
        periodEnd:
          type: string
          nullable: true
        compareCollectionId:
          type: string
          format: uuid
          nullable: true
        timezone:
          type: string
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true
      required:
        - boardType
        - targetUnits
        - breakEvenUnits
        - costBasisPoints
        - feeBasisPoints
        - periodStart
        - periodEnd
        - compareCollectionId
        - timezone
        - updatedAt
    AdminCollectionAnalyticsSettingsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminCollectionAnalyticsSettings'
      required:
        - status
        - data
    AdminCollectionAnalyticsSettingsPatch:
      type: object
      additionalProperties: false
      properties:
        boardType:
          type: string
          enum: [TICKET, PRODUCT]
          nullable: true
        targetUnits:
          type: integer
          minimum: 0
          nullable: true
        breakEvenUnits:
          type: integer
          minimum: 0
          nullable: true
        costBasisPoints:
          type: integer
          minimum: 0
          maximum: 10000
          nullable: true
        feeBasisPoints:
          type: integer
          minimum: 0
          maximum: 10000
          nullable: true
        periodStart:
          type: string
          format: date
          nullable: true
        periodEnd:
          type: string
          format: date
          nullable: true
        compareCollectionId:
          type: string
          format: uuid
          nullable: true
        timezone:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[^\r\n]*\S[^\r\n]*$'
          nullable: true
    AdminCollectionAnalyticsDailyBucket:
      type: object
      properties:
        day:
          type: string
        currency:
          type: string
        revenueCents:
          type: integer
        units:
          type: integer
        orderCount:
          type: integer
      required:
        - day
        - currency
        - revenueCents
        - units
        - orderCount
    AdminCollectionAnalyticsHourlyBucket:
      type: object
      properties:
        hour:
          type: integer
        units:
          type: integer
      required:
        - hour
        - units
    AdminCollectionAnalyticsProductRow:
      type: object
      properties:
        productId:
          type: string
          format: uuid
        name:
          type: string
        imageUrl:
          type: string
          nullable: true
        status:
          type: string
        currency:
          type: string
        priceCents:
          type: integer
          nullable: true
        initialStock:
          type: integer
          nullable: true
        units:
          type: integer
        revenueCents:
          type: integer
      required:
        - productId
        - name
        - imageUrl
        - status
        - currency
        - priceCents
        - initialStock
        - units
        - revenueCents
    AdminCollectionAnalyticsOverviewCollection:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        coverImageUrl:
          type: string
          nullable: true
      required:
        - id
        - name
        - slug
        - coverImageUrl
    AdminCollectionAnalyticsOverviewTotals:
      type: object
      properties:
        paidOrderCount:
          type: integer
        paidItemQuantity:
          type: integer
        amountsByCurrency:
          type: array
          items:
            $ref: '#/components/schemas/AdminAnalyticsCollectionAmount'
        firstPaidAt:
          type: string
          format: date-time
          nullable: true
        lastPaidAt:
          type: string
          format: date-time
          nullable: true
      required:
        - paidOrderCount
        - paidItemQuantity
        - amountsByCurrency
        - firstPaidAt
        - lastPaidAt
    AdminCollectionAnalyticsOverview:
      type: object
      properties:
        collection:
          $ref: '#/components/schemas/AdminCollectionAnalyticsOverviewCollection'
        detectedBoardType:
          type: string
          enum: [TICKET, PRODUCT]
        isTicketCollection:
          type: boolean
        effectiveTimezone:
          type: string
        settings:
          $ref: '#/components/schemas/AdminCollectionAnalyticsSettings'
        totals:
          $ref: '#/components/schemas/AdminCollectionAnalyticsOverviewTotals'
        daily:
          type: array
          items:
            $ref: '#/components/schemas/AdminCollectionAnalyticsDailyBucket'
        hourly:
          type: array
          items:
            $ref: '#/components/schemas/AdminCollectionAnalyticsHourlyBucket'
        products:
          type: array
          items:
            $ref: '#/components/schemas/AdminCollectionAnalyticsProductRow'
      required:
        - collection
        - detectedBoardType
        - isTicketCollection
        - effectiveTimezone
        - settings
        - totals
        - daily
        - hourly
        - products
    AdminCollectionAnalyticsOverviewApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminCollectionAnalyticsOverview'
      required:
        - status
        - data
    AdminPurchaseAmountByCollectionAnalyticsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminPurchaseAmountByCollectionAnalytics'
      required:
        - status
        - data
    AdminPurchaseAmountByCollectionAnalytics:
      type: object
      properties:
        paidOrderCount:
          type: integer
        paidItemQuantity:
          type: integer
        collectionCount:
          type: integer
        totalsByCurrency:
          type: array
          items:
            $ref: '#/components/schemas/AdminPurchaseAmountByCollectionAnalyticsAmount'
        collections:
          type: array
          items:
            $ref: '#/components/schemas/AdminPurchaseAmountByCollectionAnalyticsCollection'
      required:
        - paidOrderCount
        - paidItemQuantity
        - collectionCount
        - totalsByCurrency
        - collections
    AdminPurchaseAmountByCollectionAnalyticsAmount:
      type: object
      properties:
        currency:
          type: string
        productAmountCents:
          type: integer
        shippingAmountCents:
          type: integer
        totalPurchaseAmountCents:
          type: integer
      required:
        - currency
        - productAmountCents
        - shippingAmountCents
        - totalPurchaseAmountCents
    AdminPurchaseAmountByCollectionAnalyticsCollection:
      type: object
      properties:
        collectionId:
          type: string
          format: uuid
          nullable: true
        collectionName:
          type: string
        collectionSlug:
          type: string
          nullable: true
        paidOrderCount:
          type: integer
        paidItemQuantity:
          type: integer
        amountsByCurrency:
          type: array
          items:
            $ref: '#/components/schemas/AdminPurchaseAmountByCollectionAnalyticsAmount'
      required:
        - collectionId
        - collectionName
        - collectionSlug
        - paidOrderCount
        - paidItemQuantity
        - amountsByCurrency
    StoreProductCollectionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/StoreProductCollection'
      required:
        - status
        - data
    StoreProductCollectionListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductCollection'
      required:
        - status
        - data
    AdminStoreProductCollectionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminStoreProductCollection'
      required:
        - status
        - data
    AdminStoreProductCollectionListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/AdminStoreProductCollection'
      required:
        - status
        - data
    CollectionLandingInlineText:
      type: object
      additionalProperties: false
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 500
        emphasis:
          type: string
          enum: [none, strong, emphasis]
          default: none
      required:
        - text
    CollectionLandingRichTextHeading:
      type: object
      additionalProperties: false
      properties:
        kind:
          type: string
          enum: [heading]
        level:
          type: integer
          enum: [2, 3]
        content:
          type: array
          minItems: 1
          maxItems: 12
          items:
            $ref: '#/components/schemas/CollectionLandingInlineText'
      required:
        - kind
        - level
        - content
    CollectionLandingRichTextParagraph:
      type: object
      additionalProperties: false
      properties:
        kind:
          type: string
          enum: [paragraph]
        content:
          type: array
          minItems: 1
          maxItems: 24
          items:
            $ref: '#/components/schemas/CollectionLandingInlineText'
      required:
        - kind
        - content
    CollectionLandingRichTextList:
      type: object
      additionalProperties: false
      properties:
        kind:
          type: string
          enum: [list]
        style:
          type: string
          enum: [ordered, unordered]
        items:
          type: array
          minItems: 1
          maxItems: 20
          items:
            type: array
            minItems: 1
            maxItems: 12
            items:
              $ref: '#/components/schemas/CollectionLandingInlineText'
      required:
        - kind
        - style
        - items
    CollectionLandingRichTextNode:
      oneOf:
        - $ref: '#/components/schemas/CollectionLandingRichTextHeading'
        - $ref: '#/components/schemas/CollectionLandingRichTextParagraph'
        - $ref: '#/components/schemas/CollectionLandingRichTextList'
      discriminator:
        propertyName: kind
    CollectionLandingImageContent:
      type: object
      additionalProperties: false
      properties:
        src:
          type: string
          format: uri
          description: HTTPS image URL on an approved OSU Supabase host.
        alt:
          type: string
          maxLength: 240
        decorative:
          type: boolean
          default: false
        aspectRatio:
          type: string
          enum: [square, '4:3', '16:9', '21:9', '2:3', '3:4']
      required:
        - src
        - aspectRatio
    CollectionLandingHeroBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [hero]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        eyebrow:
          type: string
          minLength: 1
          maxLength: 80
        heading:
          type: string
          minLength: 1
          maxLength: 160
        kicker:
          type: string
          minLength: 1
          maxLength: 24
          description: Large display line under the heading, such as the event date.
        kickerNote:
          type: string
          minLength: 1
          maxLength: 40
          description: Small accent note beside the kicker.
        body:
          type: string
          minLength: 1
          maxLength: 600
        alignment:
          type: string
          enum: [left, center]
          default: left
        imagePosition:
          type: string
          enum: [start, end]
          description: Where a left-aligned hero image sits on wide screens; start also puts it first on narrow screens.
        image:
          $ref: '#/components/schemas/CollectionLandingImageContent'
      required:
        - id
        - type
        - heading
    CollectionLandingRichTextBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [richText]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        content:
          type: array
          minItems: 1
          maxItems: 40
          items:
            $ref: '#/components/schemas/CollectionLandingRichTextNode'
      required:
        - id
        - type
        - content
    CollectionLandingImageBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [image]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        image:
          $ref: '#/components/schemas/CollectionLandingImageContent'
        caption:
          type: string
          minLength: 1
          maxLength: 240
      required:
        - id
        - type
        - image
    CollectionLandingGalleryBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [gallery]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 320
        images:
          type: array
          minItems: 2
          maxItems: 24
          items:
            $ref: '#/components/schemas/CollectionLandingImageContent'
        layout:
          type: string
          enum: [grid, editorial, filmstrip]
          default: grid
        columns:
          type: integer
          minimum: 2
          maximum: 4
          default: 3
        previewCount:
          type: integer
          minimum: 1
          maximum: 24
        expandLabel:
          type: string
          minLength: 1
          maxLength: 40
        collapseLabel:
          type: string
          minLength: 1
          maxLength: 40
      required:
        - id
        - type
        - heading
        - images
    CollectionLandingDetailsBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [details]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 320
        items:
          type: array
          minItems: 1
          maxItems: 12
          items:
            type: object
            additionalProperties: false
            properties:
              label:
                type: string
                minLength: 1
                maxLength: 40
              value:
                type: string
                minLength: 1
                maxLength: 160
              note:
                type: string
                minLength: 1
                maxLength: 160
            required:
              - label
              - value
        columns:
          type: integer
          enum: [1, 2]
          default: 2
        emphasis:
          type: string
          enum: [none, value]
          default: none
      required:
        - id
        - type
        - items
    CollectionLandingLinksBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [links]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        items:
          type: array
          minItems: 1
          maxItems: 8
          items:
            type: object
            additionalProperties: false
            properties:
              platform:
                type: string
                enum: [instagram, threads, x, facebook, youtube, tiktok, line, website]
              href:
                type: string
                format: uri
                maxLength: 500
                description: HTTPS URL; opens in a new tab.
              label:
                type: string
                minLength: 1
                maxLength: 40
            required:
              - platform
              - href
        alignment:
          type: string
          enum: [left, center]
          default: center
      required:
        - id
        - type
        - items
    CollectionLandingAudioBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [audio]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 320
        src:
          type: string
          format: uri
          maxLength: 500
          description: HTTPS mp3, m4a, mp4, aac, or wav file on an approved OSU Supabase host.
        title:
          type: string
          minLength: 1
          maxLength: 120
          description: Track name shown in the player and announced to assistive technology.
        caption:
          type: string
          minLength: 1
          maxLength: 240
        loop:
          type: boolean
          default: false
      required:
        - id
        - type
        - src
        - title
    CollectionLandingStorefrontCtaBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [storefrontCta]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 320
        label:
          type: string
          minLength: 1
          maxLength: 80
        path:
          type: string
          minLength: 1
          maxLength: 200
          pattern: '^/(?!/)[A-Za-z0-9\-._~/?=&%]*$'
          description: Site-relative destination without a locale prefix, such as /store/another-collection. Omit to link to this collection's store page.
        alignment:
          type: string
          enum: [left, center]
          default: left
      required:
        - id
        - type
        - label
    CollectionLandingTicketSelectorBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [ticketSelector]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 320
      required:
        - id
        - type
        - heading
    CollectionLandingAutomaticProductListBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [productList]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 320
        count:
          type: integer
          minimum: 1
          maximum: 24
          default: 8
        mode:
          type: string
          enum: [automatic]
      required:
        - id
        - type
        - heading
        - mode
    CollectionLandingManualProductListBlock:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'
        type:
          type: string
          enum: [productList]
        font:
          type: string
          enum: [noto-sans, noto-serif]
        heading:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 320
        count:
          type: integer
          minimum: 1
          maximum: 24
          default: 8
        mode:
          type: string
          enum: [manual]
        productIds:
          type: array
          minItems: 1
          maxItems: 24
          uniqueItems: true
          items:
            type: string
            format: uuid
      required:
        - id
        - type
        - heading
        - mode
        - productIds
    CollectionLandingProductListBlock:
      oneOf:
        - $ref: '#/components/schemas/CollectionLandingAutomaticProductListBlock'
        - $ref: '#/components/schemas/CollectionLandingManualProductListBlock'
      discriminator:
        propertyName: mode
    CollectionLandingBlock:
      oneOf:
        - $ref: '#/components/schemas/CollectionLandingHeroBlock'
        - $ref: '#/components/schemas/CollectionLandingRichTextBlock'
        - $ref: '#/components/schemas/CollectionLandingImageBlock'
        - $ref: '#/components/schemas/CollectionLandingGalleryBlock'
        - $ref: '#/components/schemas/CollectionLandingStorefrontCtaBlock'
        - $ref: '#/components/schemas/CollectionLandingTicketSelectorBlock'
        - $ref: '#/components/schemas/CollectionLandingProductListBlock'
        - $ref: '#/components/schemas/CollectionLandingDetailsBlock'
        - $ref: '#/components/schemas/CollectionLandingLinksBlock'
        - $ref: '#/components/schemas/CollectionLandingAudioBlock'
      discriminator:
        propertyName: type
    CollectionLandingTheme:
      type: object
      additionalProperties: false
      properties:
        accent:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
        accentHover:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
        onAccent:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
        pageSurface:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
        ctaSurface:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
        foreground:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
          description: Main text colour; must reach 4.5:1 against pageSurface.
        mutedForeground:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
          description: Secondary text colour; must reach 4.5:1 against pageSurface.
        surface:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
          description: Card and placeholder surface.
        border:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
          description: Hairline and card border colour.
        headingFont:
          type: string
          enum: [serif, sans]
        heroMotion:
          type: string
          enum: [breathe, none]
        floatingCta:
          oneOf:
            - type: boolean
              enum: [false]
            - type: object
              additionalProperties: false
              properties:
                label:
                  type: string
                  minLength: 1
                  maxLength: 16
                sublabel:
                  type: string
                  minLength: 1
                  maxLength: 16
                ariaLabel:
                  type: string
                  minLength: 1
                  maxLength: 80
                background:
                  type: string
                  pattern: '^#[0-9a-fA-F]{6}$'
                foreground:
                  type: string
                  pattern: '^#[0-9a-fA-F]{6}$'
              required:
                - label
        header:
          type: object
          additionalProperties: false
          properties:
            background:
              type: string
              pattern: '^#[0-9a-fA-F]{6}$'
            foreground:
              type: string
              pattern: '^#[0-9a-fA-F]{6}$'
            accent:
              type: string
              pattern: '^#[0-9a-fA-F]{6}$'
            font:
              type: string
              enum: [noto-sans, noto-serif]
    CollectionLandingSeoImage:
      type: object
      additionalProperties: false
      properties:
        src:
          type: string
          format: uri
          pattern: '^https://(ohjalxnadyzuqpbektww|vrzekgrnobhlztdznsml)\.supabase\.co/'
        alt:
          type: string
          minLength: 1
          maxLength: 240
      required: [src, alt]
    CollectionLandingSeo:
      type: object
      additionalProperties: false
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 70
        description:
          type: string
          minLength: 1
          maxLength: 200
        image:
          $ref: '#/components/schemas/CollectionLandingSeoImage'
    CollectionLandingPageDocument:
      type: object
      additionalProperties: false
      properties:
        version:
          type: integer
          enum: [1]
        seo:
          $ref: '#/components/schemas/CollectionLandingSeo'
        theme:
          $ref: '#/components/schemas/CollectionLandingTheme'
        blocks:
          type: array
          minItems: 3
          maxItems: 24
          items:
            $ref: '#/components/schemas/CollectionLandingBlock'
      required:
        - version
        - blocks
    LandingLocale:
      type: string
      enum: [default, en, ja, ko, zh-TW, zh-CN]
    CollectionLandingDraftSaveBody:
      type: object
      additionalProperties: false
      properties:
        document:
          $ref: '#/components/schemas/CollectionLandingPageDocument'
        expectedRevision:
          type: integer
          minimum: 0
        locale:
          $ref: '#/components/schemas/LandingLocale'
      required:
        - document
        - expectedRevision
    CollectionLandingPublishBody:
      type: object
      additionalProperties: false
      properties:
        expectedRevision:
          type: integer
          minimum: 1
        locale:
          $ref: '#/components/schemas/LandingLocale'
      required:
        - expectedRevision
    CollectionLandingUnpublishBody:
      type: object
      additionalProperties: false
      properties:
        expectedPublicationVersion:
          type: integer
          minimum: 0
        locale:
          type: string
          enum: [en, ja, ko, zh-TW, zh-CN]
      required:
        - expectedPublicationVersion
        - locale
    CollectionLandingValidationIssue:
      type: object
      properties:
        path:
          type: string
        message:
          type: string
      required:
        - path
        - message
    LandingPageApiErrorResponse:
      type: object
      properties:
        status:
          type: string
          enum: [error]
        data:
          type: object
          properties:
            message:
              type: string
            code:
              type: string
              enum:
                [COLLECTION_NOT_FOUND, DEFAULT_PUBLICATION_PROTECTED, DOCUMENT_INVALID, DOCUMENT_TOO_LARGE, LOCALIZED_PUBLICATION_NOT_READY, MANUAL_PRODUCT_INVALID, PUBLICATION_NOT_FOUND, PUBLISH_PRECHECK_FAILED, STALE_PUBLICATION, STALE_REVISION, DRAFT_NOT_FOUND]
            issues:
              type: array
              items:
                $ref: '#/components/schemas/CollectionLandingValidationIssue'
          required:
            - message
            - issues
      required:
        - status
        - data
    CollectionLandingProduct:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        seoTitle:
          type: string
          nullable: true
        seoDescription:
          type: string
          nullable: true
        handle:
          type: string
          nullable: true
          maxLength: 200
        imageUrl:
          type: string
          nullable: true
        pointsCost:
          type: integer
          nullable: true
        priceCents:
          type: integer
          nullable: true
        currency:
          type: string
      required:
        - id
        - name
        - handle
        - description
        - imageUrl
        - pointsCost
        - priceCents
        - currency
    TicketDateReasonCode:
      type: string
      enum: [PAST_DATE, INVALID_DATE, DUPLICATE_DATE]
    TicketVariantReasonCode:
      type: string
      enum:
        - INACTIVE_VARIANT
        - SOLD_OUT
        - PRICE_UNAVAILABLE
        - CHECKOUT_UNAVAILABLE
    ResolvedTicketOption:
      type: object
      additionalProperties: false
      properties:
        group:
          type: string
        value:
          type: string
      required:
        - group
        - value
    ResolvedTicketVariant:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        options:
          type: array
          items:
            $ref: '#/components/schemas/ResolvedTicketOption'
        imageUrl:
          type: string
          nullable: true
        priceCents:
          type: integer
          minimum: 0
          nullable: true
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        availableQuantity:
          type: integer
          minimum: 0
          nullable: true
        purchasable:
          type: boolean
        unavailableReasons:
          type: array
          items:
            $ref: '#/components/schemas/TicketVariantReasonCode'
      required:
        - id
        - name
        - options
        - imageUrl
        - priceCents
        - currency
        - status
        - availableQuantity
        - purchasable
        - unavailableReasons
    ResolvedTicketProduct:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        date:
          type: string
          pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
          nullable: true
        timezone:
          type: string
        usesUtcFallback:
          type: boolean
        unavailableReasons:
          type: array
          items:
            $ref: '#/components/schemas/TicketDateReasonCode'
        variants:
          type: array
          items:
            $ref: '#/components/schemas/ResolvedTicketVariant'
      required:
        - id
        - name
        - date
        - timezone
        - usesUtcFallback
        - unavailableReasons
        - variants
    ResolvedTicketSelector:
      type: object
      additionalProperties: false
      properties:
        state:
          type: string
          enum: [ready, empty, error]
        products:
          type: array
          items:
            $ref: '#/components/schemas/ResolvedTicketProduct'
        defaultProductId:
          type: string
          format: uuid
          nullable: true
        defaultVariantId:
          type: string
          format: uuid
          nullable: true
      required:
        - state
        - products
        - defaultProductId
        - defaultVariantId
    ResolvedCollectionLandingPage:
      type: object
      properties:
        collection:
          $ref: '#/components/schemas/CollectionLandingCollectionRef'
        document:
          $ref: '#/components/schemas/CollectionLandingPageDocument'
        isDefault:
          type: boolean
        productBlocks:
          type: object
          additionalProperties:
            type: array
            items:
              $ref: '#/components/schemas/CollectionLandingProduct'
        ticketSelector:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ResolvedTicketSelector'
      required:
        - collection
        - document
        - isDefault
        - productBlocks
        - ticketSelector
    AdminCollectionLandingPage:
      type: object
      properties:
        collection:
          $ref: '#/components/schemas/CollectionLandingCollectionRef'
        locale:
          $ref: '#/components/schemas/LandingLocale'
        draftDocument:
          $ref: '#/components/schemas/CollectionLandingPageDocument'
        draftRevision:
          type: integer
        draftUpdatedAt:
          type: string
          format: date-time
          nullable: true
        publishedRevision:
          type: integer
          nullable: true
        publishedAt:
          type: string
          format: date-time
          nullable: true
        publicationVersion:
          type: integer
        publishedDocument:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CollectionLandingPageDocument'
        preview:
          $ref: '#/components/schemas/ResolvedCollectionLandingPage'
      required:
        - collection
        - locale
        - draftDocument
        - draftRevision
        - draftUpdatedAt
        - publishedRevision
        - publishedAt
        - publicationVersion
        - publishedDocument
        - preview
    AdminCollectionLandingUnpublishApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          allOf:
            - $ref: '#/components/schemas/AdminCollectionLandingPage'
            - type: object
              properties:
                outcome:
                  type: string
                  enum: [unpublished, already_unpublished]
              required:
                - outcome
      required:
        - status
        - data
    AdminCollectionLandingPageApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminCollectionLandingPage'
      required:
        - status
        - data
    RedemptionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/RedemptionResult'
      required:
        - status
        - data
    CheckoutSessionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            checkoutUrl:
              type: string
              description: Redirect URL to Stripe Checkout
            checkoutSessionId:
              type: string
              description: Stripe Checkout Session identifier
          required:
            - checkoutUrl
            - checkoutSessionId
      required:
        - status
        - data
    CheckoutProbeApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            sessionId:
              type: string
            checkoutStatus:
              type: string
              enum: [expired]
            durationMs:
              type: integer
              minimum: 0
          required:
            - sessionId
            - checkoutStatus
            - durationMs
      required:
        - status
        - data
    OrderListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/Order'
      required:
        - status
        - data
    AdminOrderSessionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOrderSession'
      required:
        - status
        - data
    AdminOrderSessionListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/AdminOrderSession'
        pagination:
          $ref: '#/components/schemas/PaginationMeta'
      required:
        - status
        - data
        - pagination
    AdminShippingRuleApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminShippingRule'
      required:
        - status
        - data
    AdminShippingRuleListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/AdminShippingRule'
      required:
        - status
        - data
    AdminLarkOrderWebhookIntegrationApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminLarkOrderWebhookIntegration'
      required:
        - status
        - data
    AdminLarkOrderWebhookIntegrationTestApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            integration:
              $ref: '#/components/schemas/AdminLarkOrderWebhookIntegration'
            result:
              $ref: '#/components/schemas/AdminLarkOrderWebhookTestResult'
          required:
            - integration
            - result
      required:
        - status
        - data
    CartCheckoutApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            checkoutUrl:
              type: string
              nullable: true
          required:
            - checkoutUrl
      required:
        - status
        - data
    CartApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/Cart'
      required:
        - status
        - data
    AddCartItemRequest:
      type: object
      properties:
        productId:
          type: string
          format: uuid
        variantId:
          type: string
          format: uuid
        quantity:
          type: integer
          minimum: 1
          maximum: 99
          default: 1
      required:
        - productId
        - variantId
    UpdateCartItemRequest:
      type: object
      properties:
        quantity:
          type: integer
          minimum: 0
          maximum: 99
      required:
        - quantity
    Cart:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [ACTIVE, CHECKED_OUT, ABANDONED]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: '#/components/schemas/CartItem'
        summary:
          $ref: '#/components/schemas/CartSummary'
      required:
        - id
        - status
        - createdAt
        - updatedAt
        - items
        - summary
    CheckoutShippingAddress:
      type: object
      properties:
        recipientName:
          type: string
        phone:
          type: string
        line1:
          type: string
        line2:
          type: string
          nullable: true
        city:
          type: string
        state:
          type: string
          nullable: true
        postalCode:
          type: string
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
      required:
        - recipientName
        - phone
        - line1
        - city
        - postalCode
        - country
    ShippingAddress:
      type: object
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
        recipientName:
          type: string
        phone:
          type: string
        line1:
          type: string
        line2:
          type: string
          nullable: true
        city:
          type: string
        state:
          type: string
          nullable: true
        postalCode:
          type: string
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        isDefault:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - userId
        - recipientName
        - phone
        - line1
        - line2
        - city
        - state
        - postalCode
        - country
        - isDefault
        - createdAt
        - updatedAt
    CreateShippingAddressBody:
      type: object
      properties:
        recipientName:
          type: string
        phone:
          type: string
        line1:
          type: string
        line2:
          type: string
          nullable: true
        city:
          type: string
        state:
          type: string
          nullable: true
        postalCode:
          type: string
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        isDefault:
          type: boolean
      required:
        - recipientName
        - phone
        - line1
        - city
        - postalCode
        - country
    PatchShippingAddressBody:
      type: object
      minProperties: 1
      properties:
        recipientName:
          type: string
        phone:
          type: string
        line1:
          type: string
        line2:
          type: string
          nullable: true
        city:
          type: string
        state:
          type: string
          nullable: true
        postalCode:
          type: string
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        isDefault:
          type: boolean
    ShippingQuoteBody:
      oneOf:
        - $ref: '#/components/schemas/CartShippingQuoteBody'
        - $ref: '#/components/schemas/BuyShippingQuoteBody'
      discriminator:
        propertyName: mode
        mapping:
          cart: '#/components/schemas/CartShippingQuoteBody'
          buy: '#/components/schemas/BuyShippingQuoteBody'
    CartShippingQuoteBody:
      type: object
      properties:
        mode:
          type: string
          enum: [cart]
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        couponCode:
          type: string
          description: Optional reusable collection coupon code.
        referralCode:
          type: string
          maxLength: 120
          description: Optional KOL referral code. It does not change buyer pricing.
      required:
        - mode
    BuyShippingQuoteBody:
      type: object
      properties:
        mode:
          type: string
          enum: [buy]
        productId:
          type: string
          format: uuid
        variantId:
          type: string
          format: uuid
        quantity:
          type: integer
          minimum: 1
          maximum: 10
          default: 1
          description: Exact direct-buy quantity. Omission preserves the legacy quantity of one.
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        couponCode:
          type: string
          description: Optional reusable collection coupon code.
        referralCode:
          type: string
          maxLength: 120
          description: Optional KOL referral code. It does not change buyer pricing.
      required:
        - mode
        - productId
        - variantId
    ShippingQuoteLine:
      type: object
      properties:
        title:
          type: string
        code:
          type: string
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        amountCents:
          type: integer
        currency:
          type: string
        shippingRuleId:
          type: string
          format: uuid
        shippableSubtotalCents:
          type: integer
        freeAboveSubtotalCents:
          type: integer
          nullable: true
      required:
        - title
        - code
        - country
        - amountCents
        - currency
        - shippingRuleId
        - shippableSubtotalCents
        - freeAboveSubtotalCents
    ShippingQuote:
      type: object
      properties:
        available:
          type: boolean
        required:
          type: boolean
        message:
          type: string
          nullable: true
        shippingLine:
          allOf:
            - $ref: '#/components/schemas/ShippingQuoteLine'
          nullable: true
        coupon:
          allOf:
            - $ref: '#/components/schemas/CouponQuote'
          nullable: true
      required:
        - available
        - required
        - message
        - shippingLine
        - coupon
    CouponQuote:
      type: object
      properties:
        code:
          type: string
        collectionId:
          type: string
          format: uuid
        percentOff:
          type: integer
          nullable: true
          description: Set when the coupon applies a percentage discount.
        amountOffCents:
          type: integer
          nullable: true
          description: >-
            Set when the coupon applies a fixed amount discount, in minor
            currency units deducted from each eligible unit.
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          nullable: true
          description: Set when the coupon applies a fixed amount discount.
        eligibleSubtotalCents:
          type: integer
        discountCents:
          type: integer
      required:
        - code
        - collectionId
        - percentOff
        - amountOffCents
        - currency
        - eligibleSubtotalCents
        - discountCents
    ShippingQuoteApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/ShippingQuote'
      required:
        - status
        - data
    CheckoutBody:
      type: object
      description: Shipping address is required when product category is non-digital.
      properties:
        paymentCurrency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          description: Original product currency or JPY. Defaults to the original currency.
        productId:
          type: string
          format: uuid
        variantId:
          type: string
          format: uuid
        quantity:
          type: integer
          minimum: 1
          maximum: 10
          default: 1
          description: Exact direct-buy quantity. Omission preserves the legacy quantity of one.
        guestEmail:
          type: string
          format: email
          description: Required for guest checkout.
        guestName:
          type: string
          description: Optional display name for guest checkout.
        preferredLanguage:
          type: string
          enum: [en, ja, ko, zh-TW, zh-CN]
          default: en
          description: Preferred language for guest checkout communications.
        shippingAddress:
          $ref: '#/components/schemas/CheckoutShippingAddress'
        remark:
          type: string
          maxLength: 1000
          description: Optional customer order remark.
        couponCode:
          type: string
          description: Optional reusable collection coupon code.
        referralCode:
          type: string
          maxLength: 120
          description: Optional KOL referral code. It does not change buyer pricing.
        fulfillmentMethod:
          type: string
          enum: [SHIPPING, PICKUP]
          default: SHIPPING
          description: PICKUP = paid online, handed over at the venue; no shipping address is collected and no shipping fee is charged.
      required:
        - productId
        - variantId
    ResumeCheckoutBody:
      type: object
      description: Shipping address is required when product category is non-digital.
      properties:
        orderId:
          type: string
          format: uuid
        shippingAddress:
          $ref: '#/components/schemas/CheckoutShippingAddress'
      required:
        - orderId
    CartCheckoutBody:
      type: object
      description: Shipping address is required when cart includes non-digital products.
      properties:
        paymentCurrency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          description: Original cart currency or JPY. Defaults to the original currency.
        guestEmail:
          type: string
          format: email
          description: Required for guest checkout.
        guestName:
          type: string
          description: Optional display name for guest checkout.
        preferredLanguage:
          type: string
          enum: [en, ja, ko, zh-TW, zh-CN]
          default: en
          description: Preferred language for guest checkout communications.
        shippingAddress:
          $ref: '#/components/schemas/CheckoutShippingAddress'
        remark:
          type: string
          maxLength: 1000
          description: Optional customer order remark.
        couponCode:
          type: string
          description: Optional reusable collection coupon code.
        referralCode:
          type: string
          maxLength: 120
          description: Optional KOL referral code. It does not change buyer pricing.
        fulfillmentMethod:
          type: string
          enum: [SHIPPING, PICKUP]
          default: SHIPPING
          description: PICKUP = paid online, handed over at the venue; no shipping address is collected and no shipping fee is charged.
    CheckoutPreviewBody:
      oneOf:
        - type: object
          properties:
            mode:
              type: string
              enum: [cart]
            paymentCurrency:
              type: string
              enum: [usd, hkd, cny, jpy, krw, twd]
            country:
              type: string
              enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
            couponCode:
              type: string
            referralCode:
              type: string
              maxLength: 120
            fulfillmentMethod:
              type: string
              enum: [SHIPPING, PICKUP]
              default: SHIPPING
          required: [mode]
        - type: object
          properties:
            mode:
              type: string
              enum: [buy]
            productId:
              type: string
              format: uuid
            variantId:
              type: string
              format: uuid
            quantity:
              type: integer
              minimum: 1
              maximum: 10
              default: 1
              description: Exact direct-buy quantity. Omission preserves the legacy quantity of one.
            paymentCurrency:
              type: string
              enum: [usd, hkd, cny, jpy, krw, twd]
            country:
              type: string
              enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
            couponCode:
              type: string
            referralCode:
              type: string
              maxLength: 120
            fulfillmentMethod:
              type: string
              enum: [SHIPPING, PICKUP]
              default: SHIPPING
          required: [mode, productId, variantId]
    CheckoutPreviewItem:
      type: object
      properties:
        productId: { type: string, format: uuid }
        variantId: { type: string, format: uuid }
        unitAmountCents: { type: integer }
        lineAmountCents: { type: integer }
      required: [productId, variantId, unitAmountCents, lineAmountCents]
    CheckoutPreviewCampaignPoints:
      type: object
      properties:
        campaignId: { type: string, format: uuid }
        eligibleJpy: { type: integer }
        points: { type: integer }
      required: [campaignId, eligibleJpy, points]
    CheckoutPreviewFx:
      type: object
      properties:
        provider: { type: string }
        attributionUrl: { type: string }
        providerUpdatedAt: { type: string, format: date-time }
      required: [provider, attributionUrl, providerUpdatedAt]
    CheckoutPreview:
      type: object
      properties:
        paymentCurrency: { type: string }
        items:
          type: array
          items: { $ref: '#/components/schemas/CheckoutPreviewItem' }
        subtotalCents: { type: integer }
        discountCents: { type: integer }
        shippingAmountCents: { type: integer }
        shippingMessage: { type: string, nullable: true }
        points:
          type: array
          items: { $ref: '#/components/schemas/CheckoutPreviewCampaignPoints' }
        signInToEarn: { type: boolean }
        fx:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CheckoutPreviewFx'
      required: [paymentCurrency, items, subtotalCents, discountCents, shippingAmountCents, shippingMessage, points, signInToEarn, fx]
    CheckoutPreviewApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/CheckoutPreview' }
      required: [status, data]
    JpyPricePreview:
      type: object
      properties:
        amountJpy: { type: integer }
        provider: { type: string }
        attributionUrl: { type: string }
        providerUpdatedAt: { type: string, format: date-time }
      required: [amountJpy, provider, attributionUrl, providerUpdatedAt]
    JpyPricePreviewApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/JpyPricePreview' }
      required: [status, data]
    CartItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        productId:
          type: string
          format: uuid
        variantId:
          type: string
          format: uuid
        variantName:
          type: string
        variantOptions:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariantOption'
        name:
          type: string
        description:
          type: string
          nullable: true
        imageUrl:
          type: string
          nullable: true
        meta:
          $ref: '#/components/schemas/StoreProductMeta'
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
        quantity:
          type: integer
        unitPriceCents:
          type: integer
        lineTotalCents:
          type: integer
        priceUpdated:
          type: boolean
        currency:
          type: string
        availableQuantity:
          type: integer
          nullable: true
        collection:
          allOf:
            - $ref: '#/components/schemas/StoreProductCollectionRef'
          nullable: true
      required:
        - id
        - productId
        - variantId
        - variantName
        - variantOptions
        - name
        - meta
        - category
        - quantity
        - unitPriceCents
        - lineTotalCents
        - currency
    CartSummary:
      type: object
      properties:
        itemCount:
          type: integer
        subtotalCents:
          type: integer
        currency:
          type: string
      required:
        - itemCount
        - subtotalCents
        - currency
    PaginationMeta:
      type: object
      properties:
        page:
          type: integer
          minimum: 1
        pageSize:
          type: integer
          minimum: 1
        total:
          type: integer
          minimum: 0
        totalPages:
          type: integer
          minimum: 1
      required:
        - page
        - pageSize
        - total
        - totalPages
    StoreProductMeta:
      type: object
      description: Product custom attributes. Admin-editable values are strings; creator templates may store nested JSON under reserved keys.
      additionalProperties: true
    StoreProductCreateBody:
      type: object
      description: >
        Product variants are required. All variants must use one purchase method
        (points or price), and stock is tracked per variant.
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        seoTitle:
          type: string
          nullable: true
          maxLength: 500
        seoDescription:
          type: string
          nullable: true
          maxLength: 2000
        handle:
          type: string
          minLength: 1
          maxLength: 200
        imageUrl:
          type: string
          nullable: true
        imageUrls:
          type: array
          nullable: true
          maxItems: 10
          items:
            type: string
            format: uri
        meta:
          $ref: '#/components/schemas/StoreProductMeta'
        pointsCost:
          type: integer
          nullable: true
          description: Points cost for redemption. Mutually exclusive with priceCents.
        priceCents:
          type: integer
          nullable: true
          description: Price in cents for Stripe checkout. Mutually exclusive with pointsCost.
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          default: usd
        stripeProductId:
          type: string
          description: Existing Stripe Product ID. Auto-created if omitted with priceCents.
        stripePriceId:
          type: string
          description: Existing Stripe Price ID. Auto-created if omitted with priceCents.
        earlyBirdPriceCents:
          type: integer
          nullable: true
          minimum: 1
          description: Admin-only fixed amount. Send with earlyBirdStartsAt and earlyBirdEndsAt.
        earlyBirdStartsAt:
          type: string
          format: date-time
          nullable: true
        earlyBirdEndsAt:
          type: string
          format: date-time
          nullable: true
        referralCommissionEligible:
          type: boolean
          default: false
          description: Whether a KOL referral can earn commission on this product.
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
          default: ACTIVE
        digitalDeliveryMode:
          type: string
          enum: [NONE, ATTACHMENTS]
          default: NONE
          description: Admin-only private attachment delivery mode. ATTACHMENTS requires DIGITAL and INACTIVE.
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          default: OTHER
        collectionId:
          type: string
          format: uuid
          nullable: true
          description: Optional store product collection ID.
        sharedInventoryQuantity:
          type: integer
          nullable: true
          minimum: 0
          description: Shared inventory capacity for variants with inventoryMode INHERIT. null means unlimited.
        tags:
          type: array
          maxItems: 20
          items:
            type: string
          description: >
            Raw admin tag inputs. The server applies NFKC, trim, lowercase,
            removes one optional leading #, replaces whitespace or underscore runs
            with hyphens, validates the canonical slug, deduplicates, and sorts.
        optionGroups:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductOptionGroupInput'
        variants:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/StoreProductVariantInput'
      required:
        - name
        - handle
        - optionGroups
        - variants
    AdminDigitalAttachment:
      type: object
      properties:
        id: { type: string, format: uuid }
        filename: { type: string }
        contentType:
          type: string
          enum: [image/jpeg, image/png, image/webp, image/gif, application/pdf]
        sizeBytes: { type: integer, nullable: true }
        position: { type: integer, minimum: 0 }
        state: { type: string, enum: [PENDING_UPLOAD, ACTIVE, SUPERSEDED] }
        createdAt: { type: string, format: date-time }
        uploadedAt: { type: string, format: date-time, nullable: true }
      required: [id, filename, contentType, sizeBytes, position, state, createdAt, uploadedAt]
    AdminDigitalAttachmentListApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: array
          items: { $ref: '#/components/schemas/AdminDigitalAttachment' }
      required: [status, data]
    AdminDigitalAttachmentApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/AdminDigitalAttachment' }
      required: [status, data]
    DigitalAttachmentPrepareBody:
      type: object
      properties:
        filename: { type: string, minLength: 1, maxLength: 255 }
        contentType:
          type: string
          enum: [image/jpeg, image/png, image/webp, image/gif, application/pdf]
        size: { type: integer, minimum: 1, maximum: 10485760 }
        position: { type: integer, minimum: 0, default: 0 }
        replacementAttachmentId: { type: string, format: uuid, nullable: true }
      required: [filename, contentType, size]
    DigitalAttachmentPrepareApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            attachmentId: { type: string, format: uuid }
            uploadUrl: { type: string, format: uri }
          required: [attachmentId, uploadUrl]
      required: [status, data]
    DigitalAttachmentDeleteApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            id: { type: string, format: uuid }
          required: [id]
      required: [status, data]
    StoreProductPatchBody:
      type: object
      description: >
        All fields are optional. To update variants, provide both optionGroups
        and variants together.
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        seoTitle:
          type: string
          nullable: true
          maxLength: 500
        seoDescription:
          type: string
          nullable: true
          maxLength: 2000
        handle:
          type: string
          minLength: 1
          maxLength: 200
          description: Normalized public URL handle. Duplicate handles return a conflict.
        imageUrl:
          type: string
          nullable: true
        imageUrls:
          type: array
          nullable: true
          maxItems: 10
          items:
            type: string
            format: uri
        meta:
          $ref: '#/components/schemas/StoreProductMeta'
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        digitalDeliveryMode:
          type: string
          enum: [NONE, ATTACHMENTS]
          description: Admin-only private attachment delivery mode. ATTACHMENTS requires DIGITAL and INACTIVE.
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
        collectionId:
          type: string
          format: uuid
          nullable: true
          description: Optional store product collection ID. Set null to remove collection.
        sharedInventoryQuantity:
          type: integer
          nullable: true
          minimum: 0
          description: Shared inventory capacity. Updating it requires optionGroups and the complete variants payload.
        earlyBirdPriceCents:
          type: integer
          nullable: true
          minimum: 1
          description: Admin-only fixed amount. Send all three early-bird fields together, or all null to cancel.
        earlyBirdStartsAt:
          type: string
          format: date-time
          nullable: true
        earlyBirdEndsAt:
          type: string
          format: date-time
          nullable: true
        referralCommissionEligible:
          type: boolean
          description: Whether a KOL referral can earn commission on this product.
        tags:
          type: array
          maxItems: 20
          items:
            type: string
          description: >
            Raw admin tag inputs. The server applies NFKC, trim, lowercase,
            removes one optional leading #, replaces whitespace or underscore runs
            with hyphens, validates the canonical slug, deduplicates, and sorts.
        optionGroups:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductOptionGroupInput'
        variants:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/StoreProductVariantInput'
    AdminKolReferralPatchBody:
      type: object
      required: [email, enabled, commissionBps]
      properties:
        email: { type: string, format: email }
        enabled: { type: boolean }
        commissionBps: { type: integer, minimum: 1, maximum: 10000 }
    KolReferralCodeBody:
      type: object
      required: [referralCode]
      properties:
        referralCode: { type: string, minLength: 1, maxLength: 120 }
    KolReferralCodeToggleBody:
      type: object
      required: [enabled]
      properties:
        email: { type: string, format: email }
        name: { type: string }
        enabled: { type: boolean }
    KolReferralCode:
      type: object
      required: [referralCode, enabled]
      properties:
        referralCode: { type: string, nullable: true }
        enabled: { type: boolean }
    KolReferralBalance:
      type: object
      required: [currency, pendingCents, payableCents, paidCents, negativeCarryCents, commissionCutoffAt]
      properties:
        currency: { type: string }
        pendingCents: { type: integer }
        payableCents: { type: integer }
        paidCents: { type: integer }
        negativeCarryCents: { type: integer }
        commissionCutoffAt: { type: string, format: date-time, nullable: true }
    KolReferralPayout:
      type: object
      required: [requestId, currency, amountCents, commissionCutoffAt, paidAt, createdAt]
      properties:
        requestId: { type: string, format: uuid }
        currency: { type: string }
        amountCents: { type: integer }
        commissionCutoffAt: { type: string, format: date-time }
        paidAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
    KolReferralOrder:
      type: object
      required: [id, productId, amountCents, refundedAmountCents, currency, status, createdAt, commissionCents, commissionState]
      properties:
        id: { type: string, format: uuid }
        productId: { type: string, format: uuid }
        amountCents: { type: integer }
        refundedAmountCents: { type: integer }
        currency: { type: string }
        status: { type: string }
        paidAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
        commissionCents: { type: integer }
        commissionState: { type: string, enum: [pending, payable] }
    KolReferralDashboard:
      type: object
      required: [enabled, commissionBps, referralCode, referralCodeEnabled, balances, orders, payouts]
      properties:
        enabled: { type: boolean }
        commissionBps: { type: integer, nullable: true }
        referralCode: { type: string, nullable: true }
        referralCodeEnabled: { type: boolean }
        balances: { type: array, items: { $ref: '#/components/schemas/KolReferralBalance' } }
        orders: { type: array, items: { $ref: '#/components/schemas/KolReferralOrder' } }
        payouts: { type: array, items: { $ref: '#/components/schemas/KolReferralPayout' } }
    KolReferralDashboardApiResponse:
      type: object
      required: [status, data]
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/KolReferralDashboard' }
    KolReferralCodeApiResponse:
      type: object
      required: [status, data]
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/KolReferralCode' }
    AdminKolReferralOrder:
      allOf:
        - $ref: '#/components/schemas/KolReferralOrder'
        - type: object
          properties:
            guestEmail: { type: string, nullable: true }
            buyerUserId: { type: string, nullable: true }
    AdminKolReferralPayout:
      allOf:
        - $ref: '#/components/schemas/KolReferralPayout'
        - type: object
          required: [note, paidByUserId]
          properties:
            note: { type: string }
            paidByUserId: { type: string }
    AdminKolReferralDashboard:
      type: object
      required: [email, name, enabled, commissionBps, referralCode, referralCodeEnabled, balances, orders, payouts]
      properties:
        email: { type: string, format: email }
        name: { type: string }
        enabled: { type: boolean }
        commissionBps: { type: integer, nullable: true }
        referralCode: { type: string, nullable: true }
        referralCodeEnabled: { type: boolean }
        balances: { type: array, items: { $ref: '#/components/schemas/KolReferralBalance' } }
        orders: { type: array, items: { $ref: '#/components/schemas/AdminKolReferralOrder' } }
        payouts: { type: array, items: { $ref: '#/components/schemas/AdminKolReferralPayout' } }
    KolReferralAdminApiResponse:
      type: object
      required: [status, data]
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/AdminKolReferralDashboard' }
    AdminKolReferralUpdate:
      type: object
      required: [id, enabled, commissionBps, referralCode, referralCodeEnabled]
      properties:
        id: { type: string }
        enabled: { type: boolean }
        commissionBps: { type: integer, nullable: true }
        referralCode: { type: string, nullable: true }
        referralCodeEnabled: { type: boolean }
    AdminKolReferralUpdateApiResponse:
      type: object
      required: [status, data]
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/AdminKolReferralUpdate' }
    AdminKolReferralPayoutBody:
      description: Record the actual full external transfer in currency minor units. Refunds after payment are retained as negative carry. Commission maturity is evaluated at paidAt; the server derives the cutoff and validates the amount against original unpaid earnings.
      type: object
      required: [email, requestId, currency, amountCents, paidAt, note]
      properties:
        email: { type: string, format: email }
        requestId: { type: string, format: uuid }
        amountCents: { type: integer, minimum: 1, maximum: 2147483647 }
        currency: { type: string, minLength: 1, maxLength: 12 }
        paidAt: { type: string, format: date-time }
        note: { type: string, minLength: 1, maxLength: 1000 }
    AdminKolReferralPayoutReceipt:
      type: object
      required: [payout, idempotent]
      properties:
        payout: { $ref: '#/components/schemas/AdminKolReferralPayout' }
        idempotent: { type: boolean }
    AdminKolReferralPayoutReceiptApiResponse:
      type: object
      required: [status, data]
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/AdminKolReferralPayoutReceipt' }
    StoreBulkActionBody:
      oneOf:
        - type: object
          required: [action, products]
          properties:
            action:
              type: string
              enum: [create]
            collectionId:
              type: string
              format: uuid
              nullable: true
              description: Optional default collection ID applied to rows that omit product-level collectionId.
            products:
              type: array
              minItems: 1
              maxItems: 100
              items:
                $ref: '#/components/schemas/StoreBulkCreateProductInput'
        - type: object
          required: [action, ids]
          properties:
            action:
              type: string
              enum: [delete]
            ids:
              type: array
              items:
                type: string
                format: uuid
              minItems: 1
              maxItems: 100
              uniqueItems: true
        - type: object
          required: [action, ids, patch]
          properties:
            action:
              type: string
              enum: [update]
            ids:
              type: array
              items:
                type: string
                format: uuid
              minItems: 1
              maxItems: 100
              uniqueItems: true
            patch:
              $ref: '#/components/schemas/StoreBulkUpdatePatch'
      discriminator:
        propertyName: action
    StoreBulkUpdatePatch:
      type: object
      description: >
        Subset of mutable product fields to apply to every id in the bulk
        request. At least one field must be present.
      minProperties: 1
      properties:
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
        collectionId:
          type: string
          format: uuid
          nullable: true
          description: >
            Store product collection ID to assign every selected
            product to. Pass `null` to remove the products from their
            collection. Omit the field entirely to leave it unchanged.
    StoreBulkActionApiResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          oneOf:
            - type: object
              description: Returned for action=create
              required: [created, failed]
              properties:
                created:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: IDs of products that were created successfully.
                failed:
                  type: array
                  items:
                    $ref: '#/components/schemas/StoreBulkCreateFailure'
                  description: Per-row failures when partial creation happens.
            - type: object
              description: Returned for action=delete
              required: [deleted]
              properties:
                deleted:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: IDs of products that were deleted.
            - type: object
              description: Returned for action=update
              required: [updated]
              properties:
                updated:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: IDs of products that were updated.
    StoreBulkCreateProductInput:
      type: object
      description: >
        Input row for action=create. Exactly one purchase method is required:
        provide pointsCost or priceCents.
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        seoTitle:
          type: string
          maxLength: 500
        seoDescription:
          type: string
          maxLength: 2000
        handle:
          type: string
          minLength: 1
          maxLength: 200
          description: Optional normalized public URL handle. A deterministic fallback is used when omitted.
        imageUrl:
          type: string
          nullable: true
        imageUrls:
          type: array
          nullable: true
          maxItems: 10
          items:
            type: string
            format: uri
        meta:
          $ref: '#/components/schemas/StoreProductMeta'
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
          default: ACTIVE
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          default: OTHER
        collectionId:
          type: string
          format: uuid
          nullable: true
        pointsCost:
          type: integer
          nullable: true
        priceCents:
          type: integer
          nullable: true
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          default: usd
        totalQuantity:
          type: integer
          nullable: true
      required:
        - name
    StoreBulkCreateFailure:
      type: object
      required: [index, name, message]
      properties:
        index:
          type: integer
          minimum: 0
        name:
          type: string
        message:
          type: string
    StoreProductCollectionCreateBody:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
          pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
        description:
          type: string
          nullable: true
        coverImageUrl:
          type: string
          format: uri
          nullable: true
        copyright:
          type: string
          maxLength: 500
        showInFilters:
          type: boolean
          default: true
        allowPickup:
          type: boolean
          default: false
        campaignCollectionId:
          type: string
          format: uuid
          nullable: true
          description: Parent Campaign Collection Points scope for this Marketplace Collection.
        couponCode:
          type: string
          nullable: true
          description: Admin-only normalized reusable collection coupon code.
        couponPercentOff:
          type: integer
          minimum: 1
          maximum: 99
          nullable: true
        couponStartsAt:
          type: string
          format: date-time
          nullable: true
        couponEndsAt:
          type: string
          format: date-time
          nullable: true
        couponEnabled:
          type: boolean
          default: false
        coupons:
          type: array
          items:
            $ref: '#/components/schemas/StoreCollectionCouponInput'
      required:
        - name
        - slug
    AdminShippingRule:
      type: object
      properties:
        id:
          type: string
          format: uuid
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        feeCents:
          type: integer
        freeAboveSubtotalCents:
          type: integer
          nullable: true
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - country
        - currency
        - feeCents
        - freeAboveSubtotalCents
        - status
        - createdAt
        - updatedAt
    AdminLarkOrderWebhookIntegration:
      type: object
      properties:
        enabled:
          type: boolean
        url:
          type: string
        hasSecret:
          type: boolean
        lastTestStatus:
          type: string
          enum: [success, failed]
          nullable: true
        lastTestMessage:
          type: string
          nullable: true
        lastTestedAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true
      required:
        - enabled
        - url
        - hasSecret
        - lastTestStatus
        - lastTestMessage
        - lastTestedAt
        - updatedAt
    AdminLarkOrderWebhookIntegrationPatchBody:
      type: object
      properties:
        enabled:
          type: boolean
        url:
          type: string
        secret:
          type: string
          description: Optional HMAC secret. Omit to keep the existing secret.
      required:
        - enabled
        - url
    AdminLarkOrderWebhookTestResult:
      type: object
      properties:
        ok:
          type: boolean
        status:
          type: integer
          nullable: true
        message:
          type: string
      required:
        - ok
        - status
        - message
    AdminShippingRuleCreateBody:
      type: object
      properties:
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        feeCents:
          type: integer
          minimum: 0
        freeAboveSubtotalCents:
          type: integer
          minimum: 0
          nullable: true
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
          default: ACTIVE
      required:
        - country
        - currency
        - feeCents
    AdminShippingRulePatchBody:
      type: object
      minProperties: 1
      properties:
        country:
          type: string
          enum: [US, CA, GB, AU, HK, JP, KR, TW, CN, SG]
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        feeCents:
          type: integer
          minimum: 0
        freeAboveSubtotalCents:
          type: integer
          minimum: 0
          nullable: true
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
    AdminOrderSessionActionBody:
      oneOf:
        - type: object
          properties:
            action:
              type: string
              enum: [update_status]
            status:
              type: string
              enum: [PENDING, PAID, FAILED]
          required:
            - action
            - status
        - type: object
          properties:
            action:
              type: string
              enum: [cancel]
          required:
            - action
        - type: object
          properties:
            action:
              type: string
              enum: [mark_delivered]
            delivered:
              type: boolean
              description: true marks all paid order lines in the session as delivered; false clears the marker. Staff-only fulfillment flag — the stored payment status stays PAID.
          required:
            - action
            - delivered
        - type: object
          properties:
            action:
              type: string
              enum: [refund]
            itemId:
              type: string
              description: Optional order item id for quantity-level digital refunds.
            quantity:
              type: integer
              minimum: 1
              description: Quantity to refund from the selected digital order item. If the quantity exceeds the unredeemed count, refundReason is required.
            refundReason:
              type: string
              minLength: 1
              maxLength: 500
              description: Required when refunding redeemed digital tickets, including partial exception refunds.
            mode:
              type: string
              enum: [full, service_fee]
              description: Required for full-order refunds. Partial item refunds do not use a mode.
            requestId:
              type: string
              format: uuid
              description: Required for full-order refunds and stable across retries of the same confirmation.
            expectedGrossRemainingAmountCents:
              type: integer
              minimum: 0
              description: Required full-order preview amount. A mismatch returns 409 before Stripe is called.
            expectedFeeBps:
              type: integer
              minimum: 0
              maximum: 9999
              description: Required full-order preview fee rate. A mismatch returns 409 before Stripe is called.
          required:
            - action
        - type: object
          properties:
            action:
              type: string
              enum: [update_shipping_number]
            shippingNumber:
              type: string
              minLength: 1
              maxLength: 255
              description: Tracking number saved as Shopify-style trackingNumber in order responses.
          required:
            - action
            - shippingNumber
        - type: object
          properties:
            action:
              type: string
              enum: [resend_purchase_confirmation]
              description: Resend the purchase confirmation to the server-resolved stored customer email. The client cannot provide recipient, sender, template, locale, or order data.
            requestId:
              type: string
              format: uuid
          required:
            - action
            - requestId
    AdminOrderRefundPreview:
      type: object
      properties:
        currency:
          type: string
        grossRemainingAmountCents:
          type: integer
          minimum: 0
        feeBps:
          type: integer
          minimum: 0
          maximum: 9999
        serviceFeeCents:
          type: integer
          minimum: 0
        fullRefundAmountCents:
          type: integer
          minimum: 0
        serviceFeeRefundAmountCents:
          type: integer
          minimum: 0
        serviceFeeRefundAvailable:
          type: boolean
      required:
        - currency
        - grossRemainingAmountCents
        - feeBps
        - serviceFeeCents
        - fullRefundAmountCents
        - serviceFeeRefundAmountCents
        - serviceFeeRefundAvailable
    AdminOrderRefundPreviewApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/AdminOrderRefundPreview'
      required:
        - status
        - data
    StoreProductCollectionPatchBody:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
          pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
        description:
          type: string
          nullable: true
        coverImageUrl:
          type: string
          format: uri
          nullable: true
        copyright:
          type: string
          nullable: true
          maxLength: 500
        showInFilters:
          type: boolean
        allowPickup:
          type: boolean
        campaignCollectionId:
          type: string
          format: uuid
          nullable: true
          description: Parent Campaign Collection Points scope. Re-parenting is rejected after points activity.
        couponCode:
          type: string
          nullable: true
          description: Admin-only normalized reusable collection coupon code.
        couponPercentOff:
          type: integer
          minimum: 1
          maximum: 99
          nullable: true
        couponStartsAt:
          type: string
          format: date-time
          nullable: true
        couponEndsAt:
          type: string
          format: date-time
          nullable: true
        couponEnabled:
          type: boolean
        coupons:
          type: array
          description: Full replacement list of collection coupon definitions.
          items:
            $ref: '#/components/schemas/StoreCollectionCouponInput'
    StoreCollectionCouponInput:
      type: object
      properties:
        code:
          type: string
          maxLength: 120
          description: Globally unique normalized coupon code.
        percentOff:
          type: integer
          minimum: 1
          maximum: 99
          nullable: true
          description: >-
            Percentage discount. Provide either percentOff or amountOffCents,
            not both.
        amountOffCents:
          type: integer
          minimum: 1
          nullable: true
          description: >-
            Fixed discount in minor currency units, deducted from each eligible
            unit. Provide either percentOff or amountOffCents, not both.
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          nullable: true
          description: Required for fixed discounts and null for percentage discounts.
        startsAt:
          type: string
          format: date-time
          nullable: true
        endsAt:
          type: string
          format: date-time
          nullable: true
        enabled:
          type: boolean
          default: false
      required:
        - code
    StoreCollectionCoupon:
      type: object
      properties:
        id:
          type: string
          format: uuid
        collectionId:
          type: string
          format: uuid
        code:
          type: string
        percentOff:
          type: integer
          minimum: 1
          maximum: 99
          nullable: true
        amountOffCents:
          type: integer
          minimum: 1
          nullable: true
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          nullable: true
        startsAt:
          type: string
          format: date-time
          nullable: true
        endsAt:
          type: string
          format: date-time
          nullable: true
        enabled:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - collectionId
        - code
        - percentOff
        - amountOffCents
        - currency
        - startsAt
        - endsAt
        - enabled
        - createdAt
        - updatedAt
    StoreProductCollection:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        copyright:
          type: string
          nullable: true
        coverImageUrl:
          type: string
          format: uri
          nullable: true
        showInFilters:
          type: boolean
        allowPickup:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - name
        - slug
        - coverImageUrl
        - showInFilters
    AdminStoreProductCollection:
      allOf:
        - $ref: '#/components/schemas/StoreProductCollection'
        - type: object
          properties:
            campaignId:
              type: string
              format: uuid
              nullable: true
            campaignCollectionId:
              type: string
              format: uuid
              nullable: true
            campaignCollectionName:
              type: string
              nullable: true
            couponCode:
              type: string
              nullable: true
            couponPercentOff:
              type: integer
              minimum: 1
              maximum: 99
              nullable: true
            couponStartsAt:
              type: string
              format: date-time
              nullable: true
            couponEndsAt:
              type: string
              format: date-time
              nullable: true
            couponEnabled:
              type: boolean
            coupons:
              type: array
              items:
                $ref: '#/components/schemas/StoreCollectionCoupon'
            landingDraftRevision:
              type: integer
              minimum: 0
            landingPublishedRevision:
              type: integer
              nullable: true
          required:
            - campaignId
            - couponCode
            - couponPercentOff
            - couponStartsAt
            - couponEndsAt
            - couponEnabled
            - coupons
    CollectionLandingCollectionRef:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
      required:
        - id
        - name
        - slug
        - description
    StoreProductCollectionRef:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        allowPickup:
          type: boolean
          description: Whether checkout offers the on-site pickup option for products in this collection.
      required:
        - id
        - name
        - slug
    ActiveEarlyBirdPromotion:
      type: object
      properties:
        priceCents:
          type: integer
        endsAt:
          type: string
          format: date-time
      required:
        - priceCents
        - endsAt
    StoreProduct:
      type: object
      properties:
        id:
          type: string
          format: uuid
        collectionId:
          type: string
          format: uuid
          nullable: true
        campaignCollectionId:
          type: string
          format: uuid
          nullable: true
        creatorUserId:
          type: string
          nullable: true
        name:
          type: string
        description:
          type: string
          nullable: true
        seoTitle:
          type: string
          nullable: true
        seoDescription:
          type: string
          nullable: true
        handle:
          type: string
          nullable: true
          maxLength: 200
        imageUrl:
          type: string
          nullable: true
        imageUrls:
          type: array
          maxItems: 10
          items:
            type: string
            format: uri
        meta:
          $ref: '#/components/schemas/StoreProductMeta'
        pointsCost:
          type: integer
          nullable: true
          description: Points cost for redemption. null if not available for points.
        priceCents:
          type: integer
          nullable: true
          description: Price in cents for Stripe checkout. null if not available for purchase.
        earlyBird:
          allOf:
            - $ref: '#/components/schemas/ActiveEarlyBirdPromotion'
          nullable: true
          description: Current active early-bird promotion only. Scheduled and expired rules are not public.
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
          description: Currency code for the price.
        status:
          $ref: '#/components/schemas/CreatorProductStatus'
        category:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
        digitalDeliveryMode:
          type: string
          enum: [NONE, ATTACHMENTS]
          description: Private digital attachment delivery mode. ATTACHMENTS is fulfilled through signed-in downloads.
        totalQuantity:
          type: integer
          nullable: true
          description: Total stock. null means unlimited.
        sharedInventoryQuantity:
          type: integer
          nullable: true
          description: Shared pool capacity for inherited variants. null means unlimited.
        availableQuantity:
          type: integer
          nullable: true
          description: Derived remaining stock (totalQuantity minus non-cancelled redemptions/orders). null if totalQuantity is null.
        defaultVariantId:
          type: string
          format: uuid
          nullable: true
        optionGroups:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductOptionGroup'
        variants:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariant'
        redeemedCount:
          type: integer
          description: Number of non-cancelled redemptions for this product.
        collection:
          allOf:
            - $ref: '#/components/schemas/StoreProductCollectionRef'
          nullable: true
        submittedAt:
          type: string
          format: date-time
          nullable: true
        approvedAt:
          type: string
          format: date-time
          nullable: true
        approvedByUserId:
          type: string
          nullable: true
        rejectedAt:
          type: string
          format: date-time
          nullable: true
        rejectedByUserId:
          type: string
          nullable: true
        rejectionReason:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - name
        - handle
        - status
        - category
        - digitalDeliveryMode
        - currency
        - meta
        - optionGroups
        - variants
        - redeemedCount
        - createdAt
        - updatedAt

    RedeemProductBody:
      type: object
      properties:
        variantId:
          type: string
          format: uuid
      required:
        - variantId

    StoreProductOptionGroupInput:
      type: object
      properties:
        name:
          type: string
          enum: [Color, Size]
          description: Canonical group name. The server normalizes casing and surrounding whitespace.
        values:
          type: array
          minItems: 1
          items:
            type: string
      required:
        - name
        - values

    StoreProductVariantOptionInput:
      type: object
      properties:
        group:
          type: string
          enum: [Color, Size]
          description: Canonical group name. The server normalizes casing and surrounding whitespace.
        value:
          type: string
      required:
        - group
        - value

    StoreProductVariantOption:
      type: object
      properties:
        group:
          type: string
        value:
          type: string
      required:
        - group
        - value

    StoreProductVariantInput:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        options:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariantOptionInput'
        imageUrl:
          type: string
          format: uri
          nullable: true
        pointsCost:
          type: integer
          nullable: true
        priceCents:
          type: integer
          nullable: true
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        stripeProductId:
          type: string
          nullable: true
        stripePriceId:
          type: string
          nullable: true
        totalQuantity:
          type: integer
          nullable: true
        inventoryMode:
          type: string
          enum: [INHERIT, OVERRIDE]
          default: OVERRIDE
          description: INHERIT uses the product sharedInventoryQuantity; OVERRIDE uses this variant totalQuantity.
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
      required:
        - name
        - options

    StoreProductOptionGroup:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        values:
          type: array
          items:
            type: string
      required:
        - id
        - name
        - values

    StoreProductVariant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        options:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariantOption'
        imageUrl:
          type: string
          format: uri
          nullable: true
        pointsCost:
          type: integer
          nullable: true
        priceCents:
          type: integer
          nullable: true
        effectivePriceCents:
          type: integer
          nullable: true
          description: Server-time effective price before coupons or FX.
        currency:
          type: string
          enum: [usd, hkd, cny, jpy, krw, twd]
        stripePriceId:
          type: string
          nullable: true
        totalQuantity:
          type: integer
          nullable: true
        inventoryMode:
          type: string
          enum: [INHERIT, OVERRIDE]
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        availableQuantity:
          type: integer
          nullable: true
      required:
        - id
        - name
        - options
        - imageUrl
        - currency
        - status
    DigitalDownloadAttachment:
      type: object
      properties:
        id: { type: string, format: uuid }
        filename: { type: string }
        contentType: { type: string }
        sizeBytes: { type: integer, nullable: true }
        position: { type: integer }
      required: [id, filename, contentType, sizeBytes, position]
    DigitalDownload:
      type: object
      properties:
        product:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            description: { type: string, nullable: true }
            imageUrl: { type: string, nullable: true }
            category: { type: string }
            status: { type: string }
          required: [id, name, description, imageUrl, category, status]
        availableSince: { type: string, format: date-time }
        attachments:
          type: array
          items: { $ref: '#/components/schemas/DigitalDownloadAttachment' }
      required: [product, availableSince, attachments]
    DigitalDownloadsApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: array
          items: { $ref: '#/components/schemas/DigitalDownload' }
      required: [status, data]
    DigitalDownloadUrlApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            url: { type: string, format: uri }
            expiresAt: { type: string, format: date-time }
          required: [url, expiresAt]
      required: [status, data]
    AdminPointRedemption:
      type: object
      properties:
        id: { type: string, format: uuid }
        productId: { type: string, format: uuid }
        productName: { type: string }
        variantId: { type: string, format: uuid }
        variantName: { type: string, nullable: true }
        userId: { type: string }
        userName: { type: string }
        userEmail: { type: string }
        pointsSpent: { type: integer }
        status: { type: string, enum: [FULFILLED, CANCELLED] }
        redeemedAt: { type: string, format: date-time }
      required: [id, productId, productName, variantId, userId, userName, userEmail, pointsSpent, status, redeemedAt]
    AdminPointRedemptionListApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            items:
              type: array
              items: { $ref: '#/components/schemas/AdminPointRedemption' }
            pagination:
              type: object
              properties:
                page: { type: integer }
                pageSize: { type: integer }
                total: { type: integer }
                totalPages: { type: integer }
              required: [page, pageSize, total, totalPages]
          required: [items, pagination]
      required: [status, data]
    AdminPointRedemptionCancelBody:
      type: object
      properties:
        reason: { type: string, minLength: 1, maxLength: 500 }
      required: [reason]
    AdminPointRedemptionApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            redemption: { $ref: '#/components/schemas/Redemption' }
          required: [redemption]
      required: [status, data]
    UserRedemption:
      type: object
      properties:
        id:
          type: string
          format: uuid
        productId:
          type: string
          format: uuid
        variantName:
          type: string
          nullable: true
          description: Selected variant name for identifying the item at pickup.
        productName:
          type: string
          nullable: true
        productDescription:
          type: string
          nullable: true
        productImageUrl:
          type: string
          nullable: true
        productMeta:
          allOf:
            - $ref: '#/components/schemas/StoreProductMeta'
          nullable: true
        productCategory:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          nullable: true
        collection:
          allOf:
            - $ref: '#/components/schemas/StoreProductCollectionRef'
          nullable: true
        qrCode:
          type: string
          nullable: true
          description: QR code payload for digital product or merchandise pickup redemption. Null for downloads and products without QR delivery.
        pointsSpent:
          type: integer
        status:
          type: string
          enum: [PENDING, FULFILLED, CANCELLED]
        redeemedAt:
          type: string
          format: date-time
        delivery:
          type: string
          enum: [QR, DOWNLOADS]
        myDownloadsUrl:
          type: string
          nullable: true
      required:
        - id
        - productId
        - qrCode
        - pointsSpent
        - status
        - redeemedAt
        - delivery
        - myDownloadsUrl
    UserRedemptionListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/UserRedemption'
      required:
        - status
        - data
    Redemption:
      type: object
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
        productId:
          type: string
          format: uuid
        variantId:
          type: string
          format: uuid
        qrCode:
          type: string
          nullable: true
          description: QR code payload for digital product or merchandise pickup redemption. Null for downloads and products without QR delivery.
        pointsSpent:
          type: integer
        status:
          type: string
          enum: [PENDING, FULFILLED, CANCELLED]
        redeemedAt:
          type: string
          format: date-time
        delivery:
          type: string
          enum: [QR, DOWNLOADS]
        myDownloadsUrl:
          type: string
          nullable: true
      required:
        - id
        - userId
        - productId
        - variantId
        - qrCode
        - pointsSpent
        - status
        - redeemedAt
        - delivery
        - myDownloadsUrl
    ValidateRedemptionBody:
      type: object
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 128
          description: QR code payload from a digital product or merchandise points redemption, or a paid digital ticket or on-site pickup order.
        action:
          type: string
          enum: [preview, redeem]
          description: Preview validates without mutation. Redeem consumes the requested quantity.
        quantity:
          type: integer
          minimum: 1
          description: Number of order items to redeem or collect. Defaults to all remaining unrefunded items for redeem actions.
      required:
        - code
    ValidatedRedemption:
      type: object
      properties:
        id:
          type: string
          format: uuid
        qrCode:
          type: string
        userId:
          type: string
        userName:
          type: string
        userEmail:
          type: string
        productId:
          type: string
          format: uuid
        productName:
          type: string
        productCategory:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
        collection:
          allOf:
            - $ref: '#/components/schemas/StoreProductCollectionRef'
          nullable: true
        variantId:
          type: string
          format: uuid
        variantName:
          type: string
        pointsSpent:
          type: integer
        status:
          type: string
          enum: [PENDING, FULFILLED, CANCELLED]
        redeemedAt:
          type: string
          format: date-time
      required:
        - id
        - qrCode
        - userId
        - userName
        - userEmail
        - productId
        - productName
        - productCategory
        - collection
        - variantId
        - variantName
        - pointsSpent
        - status
        - redeemedAt
    ValidatedDigitalOrder:
      type: object
      description: Validated paid digital ticket or physical on-site pickup order. The schema name is retained for client compatibility.
      properties:
        fulfillmentMethod:
          type: string
          enum: [SHIPPING, PICKUP]
          description: Physical items can be scanned only when this is PICKUP. Digital tickets retain their existing validation flow.
        id:
          type: string
          format: uuid
        qrCode:
          type: string
        qrCodeRedeemedAt:
          type: string
          format: date-time
          nullable: true
        qrCodeRedeemedQuantity:
          type: integer
        refundedQuantity:
          type: integer
        redeemedQuantity:
          type: integer
        remainingQuantity:
          type: integer
        redeemedThisScan:
          type: integer
        userId:
          type: string
          nullable: true
        userName:
          type: string
        userEmail:
          type: string
        guestName:
          type: string
          nullable: true
        guestEmail:
          type: string
          nullable: true
        productId:
          type: string
          format: uuid
        productName:
          type: string
        productCategory:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
        collection:
          allOf:
            - $ref: '#/components/schemas/StoreProductCollectionRef'
          nullable: true
        variantId:
          type: string
          format: uuid
        variantName:
          type: string
        quantity:
          type: integer
        amountCents:
          type: integer
        currency:
          type: string
        status:
          type: string
          enum: [PENDING, PAID, FAILED, REFUNDED]
        paidAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - qrCode
        - qrCodeRedeemedAt
        - qrCodeRedeemedQuantity
        - refundedQuantity
        - redeemedQuantity
        - remainingQuantity
        - redeemedThisScan
        - userId
        - userName
        - userEmail
        - guestName
        - guestEmail
        - productId
        - productName
        - productCategory
        - collection
        - variantId
        - variantName
        - quantity
        - amountCents
        - currency
        - status
        - paidAt
        - createdAt
    ValidateRedemptionResult:
      type: object
      properties:
        kind:
          type: string
          enum: [redemption, order]
        redemption:
          allOf:
            - $ref: '#/components/schemas/ValidatedRedemption'
          nullable: true
        order:
          allOf:
            - $ref: '#/components/schemas/ValidatedDigitalOrder'
          nullable: true
        validatedAt:
          type: string
          format: date-time
      required:
        - kind
        - validatedAt
    ValidateRedemptionApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/ValidateRedemptionResult'
      required:
        - status
        - data
    OrderItem:
      type: object
      properties:
        fulfillmentMethod:
          type: string
          enum: [SHIPPING, PICKUP]
        id:
          type: string
          format: uuid
        productId:
          type: string
          format: uuid
        variantId:
          type: string
          format: uuid
        variantName:
          type: string
          nullable: true
        variantOptions:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariantOption'
        productName:
          type: string
          nullable: true
        productDescription:
          type: string
          nullable: true
        productImageUrl:
          type: string
          nullable: true
        productMeta:
          allOf:
            - $ref: '#/components/schemas/StoreProductMeta'
          nullable: true
        productCategory:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          nullable: true
        quantity:
          type: integer
        amountCents:
          type: integer
        currency:
          type: string
        qrCode:
          type: string
          nullable: true
          description: QR code payload generated after payment for a digital ticket or a physical order with on-site pickup. Shipped physical orders do not receive a code.
        qrCodeRedeemedAt:
          type: string
          format: date-time
          nullable: true
        qrCodeRedeemedQuantity:
          type: integer
        refundedQuantity:
          type: integer
        refundedAmountCents:
          type: integer
        refundableQuantity:
          type: integer
        requiresShipping:
          type: boolean
        fulfillmentStatus:
          type: string
          enum: [fulfilled]
          nullable: true
      required:
        - id
        - productId
        - variantId
        - variantOptions
        - quantity
        - amountCents
        - currency
        - qrCode
        - qrCodeRedeemedAt
        - qrCodeRedeemedQuantity
        - refundedQuantity
        - refundedAmountCents
        - refundableQuantity
        - requiresShipping
        - fulfillmentStatus
    OrderShippingLine:
      type: object
      properties:
        title:
          type: string
          example: Standard
        code:
          type: string
          example: standard
        amountCents:
          type: integer
        currency:
          type: string
        country:
          type: string
          nullable: true
        shippableSubtotalCents:
          type: integer
          nullable: true
        freeAboveSubtotalCents:
          type: integer
          nullable: true
      required:
        - title
        - code
        - amountCents
        - currency
    Order:
      type: object
      properties:
        fulfillmentMethod:
          type: string
          enum: [SHIPPING, PICKUP]
        id:
          type: string
          format: uuid
        stripeSessionId:
          type: string
        products:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        lineItems:
          type: array
          description: Shopify-style alias for products.
          items:
            $ref: '#/components/schemas/OrderItem'
        productId:
          type: string
          format: uuid
        productName:
          type: string
          nullable: true
        productDescription:
          type: string
          nullable: true
        productImageUrl:
          type: string
          nullable: true
        productMeta:
          allOf:
            - $ref: '#/components/schemas/StoreProductMeta'
          nullable: true
        productCategory:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          nullable: true
        amountCents:
          type: integer
        quantity:
          type: integer
        currency:
          type: string
        qrCode:
          type: string
          nullable: true
          description: QR code payload generated after payment for a digital ticket or a physical order with on-site pickup. Shipped physical orders do not receive a code.
        qrCodeRedeemedAt:
          type: string
          format: date-time
          nullable: true
        qrCodeRedeemedQuantity:
          type: integer
        refundedQuantity:
          type: integer
        refundedAmountCents:
          type: integer
        purchasePointsEarned:
          type: integer
        status:
          type: string
          enum: [PENDING, PAID, FAILED, REFUNDED]
          description: Legacy payment status. Prefer financialStatus for Shopify-style integrations.
        financialStatus:
          type: string
          enum: [pending, paid, voided, refunded]
        fulfillmentStatus:
          type: string
          enum: [fulfilled, partial]
          nullable: true
        paidAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        shippingAddress:
          allOf:
            - $ref: '#/components/schemas/OrderShippingAddress'
          nullable: true
        shippingLines:
          type: array
          items:
            $ref: '#/components/schemas/OrderShippingLine'
      required:
        - id
        - stripeSessionId
        - products
        - lineItems
        - productId
        - amountCents
        - quantity
        - currency
        - qrCode
        - qrCodeRedeemedAt
        - qrCodeRedeemedQuantity
        - refundedQuantity
        - refundedAmountCents
        - purchasePointsEarned
        - status
        - financialStatus
        - fulfillmentStatus
        - createdAt
        - shippingAddress
        - shippingLines
    RedemptionResult:
      type: object
      properties:
        redemption:
          $ref: '#/components/schemas/Redemption'
        officialPoints:
          type: integer
        campaignPoints:
          type: integer
        pointsSpent:
          type: integer
        remainingPoints:
          type: integer
        outcome:
          type: string
          enum: [REDEEMED, ALREADY_OWNED]
        delivery:
          type: string
          enum: [QR, DOWNLOADS]
        myDownloadsUrl:
          type: string
          nullable: true
      required:
        - redemption
        - officialPoints
        - campaignPoints
        - pointsSpent
        - remainingPoints
        - outcome
        - delivery
        - myDownloadsUrl
    OrderHistoryItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        productId:
          type: string
          format: uuid
        productName:
          type: string
          nullable: true
        productDescription:
          type: string
          nullable: true
        productImageUrl:
          type: string
          nullable: true
        productMeta:
          allOf:
            - $ref: '#/components/schemas/StoreProductMeta'
          nullable: true
        productCategory:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          nullable: true
        amountCents:
          type: integer
        quantity:
          type: integer
        currency:
          type: string
        refundedQuantity:
          type: integer
        refundedAmountCents:
          type: integer
        purchasePointsEarned:
          type: integer
        status:
          type: string
          enum: [PENDING, PAID, FAILED, REFUNDED]
        paidAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        shippingAddress:
          allOf:
            - $ref: '#/components/schemas/OrderShippingAddress'
          nullable: true
      required:
        - id
        - productId
        - amountCents
        - quantity
        - currency
        - refundedQuantity
        - refundedAmountCents
        - purchasePointsEarned
        - status
        - createdAt
        - shippingAddress
    OrderShippingAddress:
      type: object
      properties:
        recipientName:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        line1:
          type: string
          nullable: true
        line2:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        postalCode:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        shippingNumber:
          type: string
          nullable: true
          deprecated: true
          description: Legacy field. Prefer trackingNumber.
        trackingNumber:
          type: string
          nullable: true
        trackingCompany:
          type: string
          nullable: true
        trackingUrl:
          type: string
          nullable: true
      required:
        - recipientName
        - email
        - phone
        - line1
        - line2
        - city
        - state
        - postalCode
        - country
        - shippingNumber
        - trackingNumber
        - trackingCompany
        - trackingUrl
    AdminOrderSessionItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        productId:
          type: string
          format: uuid
        variantId:
          type: string
          format: uuid
        variantName:
          type: string
          nullable: true
        variantOptions:
          type: array
          items:
            $ref: '#/components/schemas/StoreProductVariantOption'
        productName:
          type: string
          nullable: true
        productImageUrl:
          type: string
          nullable: true
        productMeta:
          allOf:
            - $ref: '#/components/schemas/StoreProductMeta'
          nullable: true
        productCategory:
          type: string
          enum: [MERCHANDISE, DIGITAL, EXPERIENCE, OTHER]
          nullable: true
        quantity:
          type: integer
        amountCents:
          type: integer
        discountAmountCents:
          type: integer
        couponCode:
          type: string
          nullable: true
        couponPercentOff:
          type: integer
          nullable: true
        couponCollectionId:
          type: string
          format: uuid
          nullable: true
        currency:
          type: string
        status:
          type: string
          enum: [PENDING, PAID, DELIVERED, FAILED, REFUNDED, MIXED]
          description: DELIVERED is derived for display from a paid order line whose deliveredAt marker is set; the stored payment status stays PAID.
        qrCodeRedeemedQuantity:
          type: integer
        refundedQuantity:
          type: integer
        refundedAmountCents:
          type: integer
        refundableQuantity:
          type: integer
        requiresShipping:
          type: boolean
        fulfillmentStatus:
          type: string
          enum: [fulfilled]
          nullable: true
        deliveredAt:
          type: string
          format: date-time
          nullable: true
      required:
        - id
        - productId
        - variantId
        - variantOptions
        - quantity
        - amountCents
        - discountAmountCents
        - couponCode
        - couponPercentOff
        - couponCollectionId
        - currency
        - status
        - qrCodeRedeemedQuantity
        - refundedQuantity
        - refundedAmountCents
        - refundableQuantity
        - requiresShipping
        - fulfillmentStatus
        - deliveredAt
    AdminOrderSession:
      type: object
      properties:
        firstOrderId:
          type: string
          format: uuid
        stripeSessionId:
          type: string
        stripePaymentIntentId:
          type: string
          nullable: true
        userId:
          type: string
        userName:
          type: string
        userEmail:
          type: string
        userPreferredLanguage:
          type: string
        remark:
          type: string
          nullable: true
        status:
          type: string
          enum: [PENDING, PAID, DELIVERED, FAILED, REFUNDED, MIXED]
          description: DELIVERED is derived for display when every paid line in the session carries the staff deliveredAt marker; the stored payment status stays PAID.
        financialStatus:
          type: string
          enum: [pending, paid, voided, refunded, mixed]
        fulfillmentStatus:
          type: string
          enum: [fulfilled, partial]
          nullable: true
        fulfillmentMethod:
          type: string
          enum: [SHIPPING, PICKUP]
          description: PICKUP sessions are paid online and handed over at the venue; they carry no shipping address.
        createdAt:
          type: string
          format: date-time
        paidAt:
          type: string
          format: date-time
          nullable: true
        deliveredAt:
          type: string
          format: date-time
          nullable: true
        isDelivered:
          type: boolean
          description: Whether every paid line eligible for delivery is marked delivered, independent of the aggregate session status.
        totalAmountCents:
          type: integer
        totalDiscountAmountCents:
          type: integer
        usedCoupon:
          type: boolean
        couponCode:
          type: string
          nullable: true
        couponCodes:
          type: array
          items:
            type: string
        purchasePointsEarned:
          type: integer
        totalQuantity:
          type: integer
        currency:
          type: string
        orderCount:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/AdminOrderSessionItem'
        shippingAddress:
          allOf:
            - $ref: '#/components/schemas/OrderShippingAddress'
          nullable: true
        shippingLines:
          type: array
          items:
            $ref: '#/components/schemas/OrderShippingLine'
        canCancel:
          type: boolean
        canRefund:
          type: boolean
        canMarkDelivered:
          type: boolean
      required:
        - firstOrderId
        - stripeSessionId
        - stripePaymentIntentId
        - userId
        - userName
        - userEmail
        - userPreferredLanguage
        - remark
        - status
        - financialStatus
        - fulfillmentStatus
        - createdAt
        - paidAt
        - deliveredAt
        - isDelivered
        - totalAmountCents
        - totalDiscountAmountCents
        - usedCoupon
        - couponCode
        - couponCodes
        - purchasePointsEarned
        - totalQuantity
        - currency
        - orderCount
        - items
        - shippingAddress
        - shippingLines
        - canCancel
        - canRefund
        - canMarkDelivered
    PointsData:
      type: object
      properties:
        pointsSpent:
          type: integer
        availablePoints:
          type: integer
        scopedBalances:
          type: array
          items:
            $ref: '#/components/schemas/ScopedPointBalance'
        cutoverActive:
          type: boolean
      required:
        - pointsSpent
        - availablePoints
        - scopedBalances
        - cutoverActive
    ScopedPointBalance:
      type: object
      properties:
        campaignCollectionId:
          type: string
          format: uuid
        currentPoints:
          type: integer
      required:
        - campaignCollectionId
        - currentPoints
    AccountData:
      type: object
      properties:
        firstName:
          type: string
          nullable: true
        lastName:
          type: string
          nullable: true
        email:
          type: string
        profileUrl:
          type: string
          nullable: true
        isVerified:
          type: boolean
          nullable: true
      required:
        - email
    ProfileData:
      type: object
      properties:
        firstName:
          type: string
          nullable: true
        lastName:
          type: string
          nullable: true
        email:
          type: string
        emailVerified:
          type: boolean
        preferredLanguage:
          type: string
          enum: [en, ja, ko, zh-TW, zh-CN]
        marketingEmailsEnabled:
          type: boolean
        marketingEmailOptedInAt:
          type: string
          format: date-time
          nullable: true
        marketingEmailUnsubscribedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        officialPoints:
          type: integer
        campaignPoints:
          type: integer
        availablePoints:
          type: integer
        campaignsJoined:
          type: integer
        threadsUsername:
          type: string
          nullable: true
        threadsConnectedAt:
          type: string
          format: date-time
          nullable: true
        threadsProfileUrl:
          type: string
          nullable: true
        isVerified:
          type: boolean
          nullable: true
      required:
        - email
        - emailVerified
        - preferredLanguage
        - marketingEmailsEnabled
        - marketingEmailOptedInAt
        - marketingEmailUnsubscribedAt
        - createdAt
        - updatedAt
        - officialPoints
        - campaignPoints
        - availablePoints
        - campaignsJoined
    LeaderboardPost:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        profilePictureUrl:
          type: string
          nullable: true
        isVerified:
          type: boolean
          nullable: true
        mediaType:
          type: string
        mediaUrl:
          type: string
          nullable: true
        permalink:
          type: string
        content:
          type: string
          nullable: true
        shortcode:
          type: string
          nullable: true
        thumbnailUrl:
          type: string
          nullable: true
        postedAt:
          type: string
          format: date-time
        isQuotePost:
          type: boolean
          nullable: true
        likeCount:
          type: integer
          nullable: true
        replyCount:
          type: integer
          nullable: true
        repostCount:
          type: integer
          nullable: true
        shareCount:
          type: integer
          nullable: true
        quoteCount:
          type: integer
          nullable: true
        score:
          type: integer
        sentiment:
          type: string
          enum: [positive, negative]
        hashtags:
          type: array
          items:
            type: string
      required:
        - id
        - mediaType
        - permalink
        - postedAt
        - score
        - sentiment
        - hashtags
    MyThreadsPostsApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/MyThreadsPost'
      required:
        - status
        - data
    AdminThreadsPostListApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/AdminThreadsPost'
            total:
              type: integer
            page:
              type: integer
            limit:
              type: integer
          required:
            - items
            - total
            - page
            - limit
      required:
        - status
        - data
    AdminThreadsPost:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        profilePictureUrl:
          type: string
          nullable: true
        mediaType:
          type: string
        mediaUrl:
          type: string
          nullable: true
        storedMediaUrl:
          type: string
          nullable: true
        permalink:
          type: string
        content:
          type: string
          nullable: true
        shortcode:
          type: string
          nullable: true
        thumbnailUrl:
          type: string
          nullable: true
        storedThumbnailUrl:
          type: string
          nullable: true
        postedAt:
          type: string
          format: date-time
        isQuotePost:
          type: boolean
        likeCount:
          type: integer
          nullable: true
        replyCount:
          type: integer
          nullable: true
        repostCount:
          type: integer
          nullable: true
        quoteCount:
          type: integer
          nullable: true
        score:
          type: integer
        sentiment:
          type: string
          enum: [positive, negative]
        hashtags:
          type: array
          items:
            type: string
        status:
          type: string
          nullable: true
      required:
        - id
        - mediaType
        - permalink
        - postedAt
        - isQuotePost
        - score
        - sentiment
        - hashtags
    MyThreadsPost:
      type: object
      properties:
        id:
          type: string
        content:
          type: string
          nullable: true
        permalink:
          type: string
        mediaUrl:
          type: string
          nullable: true
        thumbnailUrl:
          type: string
          nullable: true
        postedAt:
          type: string
          format: date-time
        likeCount:
          type: integer
          nullable: true
        replyCount:
          type: integer
          nullable: true
        repostCount:
          type: integer
          nullable: true
        shareCount:
          type: integer
          nullable: true
      required:
        - id
        - permalink
        - postedAt
    OverviewData:
      type: object
      properties:
        officialPoints:
          type: integer
        campaignPoints:
          type: integer
        pointHistory:
          $ref: '#/components/schemas/PointHistory'
        contributionsHistory:
          type: array
          items:
            $ref: '#/components/schemas/ContributionSummary'
        recommendedCampaigns:
          type: array
          items:
            $ref: '#/components/schemas/CampaignSummary'
        collectionPointHoldings:
          type: array
          items:
            $ref: '#/components/schemas/CollectionPointHolding'
        collectionPointHistory:
          $ref: '#/components/schemas/CollectionPointHistory'
        runningCampaigns:
          type: array
          items:
            $ref: '#/components/schemas/RunningCampaign'
      required:
        - officialPoints
        - campaignPoints
        - pointHistory
        - contributionsHistory
        - recommendedCampaigns
        - collectionPointHoldings
        - collectionPointHistory
        - runningCampaigns
    PointHistory:
      type: object
      properties:
        startDate:
          type: string
          format: date
        endDate:
          type: string
          format: date
        timeZone:
          type: string
          enum: [UTC]
        granularity:
          type: string
          enum: [day]
        officialPointsSeries:
          $ref: '#/components/schemas/PointHistorySeries'
        campaignPointsSeries:
          $ref: '#/components/schemas/PointHistorySeries'
        buckets:
          type: array
          minItems: 30
          maxItems: 30
          items:
            $ref: '#/components/schemas/PointHistoryBucket'
      required:
        - startDate
        - endDate
        - timeZone
        - granularity
        - officialPointsSeries
        - campaignPointsSeries
        - buckets
    PointHistorySeries:
      type: object
      properties:
        status:
          type: string
          enum: [available, unavailable]
        reason:
          type: string
          nullable: true
          enum:
            - historical_semantics_not_reconstructable
            - legacy_period_in_range
            - ledger_reconciliation_failed
      required:
        - status
        - reason
    PointHistoryBucket:
      type: object
      properties:
        date:
          type: string
          format: date
        officialPoints:
          type: integer
          nullable: true
        campaignPoints:
          type: integer
          nullable: true
      required:
        - date
        - officialPoints
        - campaignPoints
    CollectionPointHolding:
      type: object
      properties:
        campaignCollectionId:
          type: string
          format: uuid
        name:
          type: string
        logoUrl:
          type: string
          nullable: true
        currentPoints:
          type: integer
          minimum: 0
        change30d:
          type: integer
          nullable: true
      required:
        - campaignCollectionId
        - name
        - logoUrl
        - currentPoints
        - change30d
    CollectionPointSeriesBucket:
      type: object
      properties:
        date:
          type: string
          format: date
        balance:
          type: integer
          nullable: true
      required:
        - date
        - balance
    CollectionPointSeries:
      type: object
      properties:
        campaignCollectionId:
          type: string
          format: uuid
        name:
          type: string
        logoUrl:
          type: string
          nullable: true
        status:
          type: string
          enum: [available, unavailable]
        reason:
          type: string
          nullable: true
          enum: [legacy_period_in_range, ledger_reconciliation_failed]
        buckets:
          type: array
          minItems: 30
          maxItems: 30
          items:
            $ref: '#/components/schemas/CollectionPointSeriesBucket'
      required:
        - campaignCollectionId
        - name
        - logoUrl
        - status
        - reason
        - buckets
    CollectionPointHistory:
      type: object
      properties:
        startDate:
          type: string
          format: date
        endDate:
          type: string
          format: date
        timeZone:
          type: string
          enum: [UTC]
        granularity:
          type: string
          enum: [day]
        series:
          type: array
          items:
            $ref: '#/components/schemas/CollectionPointSeries'
      required:
        - startDate
        - endDate
        - timeZone
        - granularity
        - series
    RunningCampaign:
      allOf:
        - $ref: '#/components/schemas/CampaignSummary'
        - type: object
          properties:
            joined:
              type: boolean
          required:
            - joined
    PointTransactionCollection:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        logoUrl:
          type: string
          nullable: true
      required:
        - id
        - name
        - logoUrl
    PointTransaction:
      type: object
      properties:
        id:
          type: string
          format: uuid
        occurredAt:
          type: string
          format: date-time
        entryType:
          type: string
          enum: [AWARD, REVERSAL, REDEEM, RELEASE, IMPORT, LEGACY]
        pointsDelta:
          type: integer
        resultingBalance:
          type: integer
        sourceLabel:
          type: string
        collection:
          $ref: '#/components/schemas/PointTransactionCollection'
      required:
        - id
        - occurredAt
        - entryType
        - pointsDelta
        - resultingBalance
        - sourceLabel
        - collection
    PointTransactionPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PointTransaction'
        nextCursor:
          type: string
          nullable: true
      required:
        - items
        - nextCursor
    PointTransactionPageApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/PointTransactionPage'
      required:
        - status
        - data
    # Threads Schemas
    ThreadsInsightValue:
      type: object
      properties:
        value:
          type: number
        end_time:
          type: string
      required:
        - value
    ThreadsInsightMetric:
      type: object
      properties:
        name:
          type: string
        period:
          type: string
        values:
          type: array
          items:
            $ref: '#/components/schemas/ThreadsInsightValue'
        title:
          type: string
        description:
          type: string
        id:
          type: string
      required:
        - name
        - period
        - values
        - title
        - description
        - id
    ThreadsInsightsResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ThreadsInsightMetric'
      required:
        - data
    ThreadsSyncData:
      type: object
      properties:
        synced:
          type: integer
          description: Number of posts synced
        lastSyncedAt:
          type: string
          description: ISO 8601 timestamp of the sync
      required:
        - synced
        - lastSyncedAt
    ThreadsSyncApiResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/ThreadsSyncData'
      required:
        - status
        - data
    ThreadsPost:
      type: object
      properties:
        id:
          type: string
        media_product_type:
          type: string
        media_type:
          type: string
        media_url:
          type: string
        permalink:
          type: string
        username:
          type: string
        text:
          type: string
        timestamp:
          type: string
        shortcode:
          type: string
        thumbnail_url:
          type: string
      required:
        - id
        - permalink
        - timestamp
        - shortcode
    ThreadsPaging:
      type: object
      properties:
        cursors:
          type: object
          properties:
            before:
              type: string
            after:
              type: string
        next:
          type: string
        previous:
          type: string
    ThreadsPostsResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ThreadsPost'
        paging:
          $ref: '#/components/schemas/ThreadsPaging'
      required:
        - data
    CampaignRedirectLink:
      type: object
      properties:
        label:
          type: string
        url:
          type: string
        tracking:
          type: string
      required:
        - label
        - url
    CampaignListData:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CampaignSummary'
        total:
          type: integer
          description: Total number of campaigns matching the query
        page:
          type: integer
          description: Current page number
        limit:
          type: integer
          description: Number of items per page
        totalPages:
          type: integer
          description: Total number of pages
      required:
        - items
        - total
        - page
        - limit
        - totalPages
    CampaignSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        poster:
          allOf:
            - $ref: '#/components/schemas/CampaignPoster'
          nullable: true
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time
        totalPointsPool:
          type: integer
        rankMode:
          type: string
          enum: [TOP_PERFORMERS, ENGAGEMENT_BASED]
        hashtags:
          type: array
          items:
            type: string
        imageCount:
          type: integer
        videoCount:
          type: integer
        redirectLinks:
          type: array
          items:
            $ref: '#/components/schemas/CampaignRedirectLink'
        creatorCount:
          type: integer
        status:
          type: string
          enum: [DRAFT, ACTIVE, PAUSED, COMPLETED, CANCELLED]
        createdAt:
          type: string
          format: date-time
        collection:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CampaignSummaryCollection'
      required:
        - id
        - title
        - slug
        - startDate
        - endDate
        - totalPointsPool
        - rankMode
        - hashtags
        - imageCount
        - videoCount
        - redirectLinks
        - creatorCount
        - status
        - createdAt
    CampaignSummaryCollection:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        logoUrl:
          type: string
          nullable: true
        company:
          type: string
          nullable: true
      required:
        - id
        - name
    ContributionSummary:
      type: object
      properties:
        id:
          type: string
        campaignId:
          type: string
        campaignTitle:
          type: string
          nullable: true
        hashtags:
          type: array
          items:
            type: string
        osuPoints:
          type: integer
          description: OSU Coins earned for this contribution (postsCreated × 10)
        campaignPoints:
          type: integer
          description: Confirmed fixed campaign check-in reward plus points earned from campaign engagement weights
        totalViews:
          type: integer
        totalEngagement:
          type: integer
        likesCount:
          type: integer
        commentsCount:
          type: integer
        repostsCount:
          type: integer
        sharesCount:
          type: integer
        joinedAt:
          type: string
        updatedAt:
          type: string
        status:
          type: string
          enum: [ONGOING, COMPLETED, CANCELLED]
      required:
        - id
        - campaignId
        - osuPoints
        - campaignPoints
        - totalViews
        - totalEngagement
        - likesCount
        - commentsCount
        - repostsCount
        - sharesCount
        - joinedAt
        - updatedAt
        - status
    ContributionDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
        campaignId:
          type: string
          format: uuid
        campaignTitle:
          type: string
        status:
          type: string
          enum: [Ongoing, Completed, Cancelled]
        joinedDate:
          type: string
          format: date-time
        username:
          type: string
          nullable: true
        profilePictureUrl:
          type: string
          nullable: true
        osuPoints:
          type: integer
          description: OSU Coins earned for this contribution (postsCreated × 10)
        campaignPoints:
          type: integer
          description: Confirmed fixed campaign check-in reward plus points earned from campaign engagement weights
        hashtags:
          type: array
          items:
            type: string
        stats:
          $ref: '#/components/schemas/ContributionDetailStats'
        engagement:
          $ref: '#/components/schemas/ContributionDetailEngagement'
        posts:
          type: array
          items:
            $ref: '#/components/schemas/ContributionDetailPost'
      required:
        - id
        - campaignId
        - campaignTitle
        - status
        - joinedDate
        - username
        - profilePictureUrl
        - osuPoints
        - campaignPoints
        - hashtags
        - stats
        - engagement
        - posts
    ContributionDetailStats:
      type: object
      properties:
        totalPosts:
          type: integer
        totalEngagement:
          type: integer
        avgEngagement:
          type: integer
      required:
        - totalPosts
        - totalEngagement
        - avgEngagement
    ContributionDetailEngagement:
      type: object
      properties:
        likes:
          type: integer
        comments:
          type: integer
        reposts:
          type: integer
        shares:
          type: integer
      required:
        - likes
        - comments
        - reposts
        - shares
    ContributionDetailPost:
      type: object
      properties:
        id:
          type: string
        permalink:
          type: string
        date:
          type: string
          format: date-time
        content:
          type: string
        osuPoints:
          type: integer
          description: OSU Coins earned for this post (OSU_POST_POINTS = 10)
        campaignPoints:
          type: integer
          description: Campaign engagement points for this post (likes×1 + comments×3 + reposts×1.5)
        likes:
          type: integer
        comments:
          type: integer
        reposts:
          type: integer
        shares:
          type: integer
        tags:
          type: array
          items:
            type: string
      required:
        - id
        - permalink
        - date
        - content
        - osuPoints
        - campaignPoints
        - likes
        - comments
        - reposts
        - shares
        - tags
    # Collection & Campaign Status Schemas
    CampaignStatusCounts:
      type: object
      properties:
        active:
          type: integer
          description: Number of active campaigns (status ACTIVE and endDate in future)
        completed:
          type: integer
          description: Number of completed campaigns (status COMPLETED)
        expired:
          type: integer
          description: Number of expired campaigns (endDate passed but status still ACTIVE)
      required:
        - active
        - completed
        - expired
    CollectionWithStats:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        campaigns:
          $ref: '#/components/schemas/CampaignStatusCounts'
      required:
        - id
        - name
        - campaigns
    # Campaign Detail Schemas
    CampaignDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
          nullable: true
        info:
          $ref: '#/components/schemas/CampaignInfo'
          description: Detailed campaign information configured in admin CMS.
        participation_method:
          $ref: '#/components/schemas/CampaignParticipationMethod'
        featuredProducts:
          type: array
          items:
            $ref: '#/components/schemas/CampaignFeaturedProduct'
          description: Participation method configuration from admin CMS.
        storeCollectionSlug:
          type: string
          nullable: true
          description: >-
            Slug of the store collection the featured products belong to. The
            campaign detail page links its "see everything" action to
            /store/{storeCollectionSlug}; null falls back to the whole store.
        gifts:
          type: array
          description: Reward items mapped from campaign rewards list. Returns empty array when rewards is null.
          items:
            type: string
        status:
          type: string
          enum: [ACTIVE, COMPLETED]
        joined:
          type: boolean
        collection:
          $ref: '#/components/schemas/CampaignCollection'
          nullable: true
        publisher:
          $ref: '#/components/schemas/CampaignPublisher'
          nullable: true
        configuration:
          $ref: '#/components/schemas/CampaignConfiguration'
        timeline:
          $ref: '#/components/schemas/CampaignTimeline'
        rewards:
          $ref: '#/components/schemas/CampaignRewards'
        statistics:
          $ref: '#/components/schemas/CampaignStatistics'
        participation:
          $ref: '#/components/schemas/CampaignParticipation'
        topContributors:
          type: array
          items:
            $ref: '#/components/schemas/TopContributor'
        creatorCount:
          type: integer
      required:
        - id
        - title
        - status
        - joined
        - info
        - participation_method
        - gifts
        - configuration
        - timeline
        - rewards
        - statistics
        - participation
        - topContributors
        - creatorCount
    CampaignCollection:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        company:
          type: string
          nullable: true
        website:
          type: string
          nullable: true
        logoUrl:
          type: string
          nullable: true
        bannerUrl:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
      required:
        - id
        - name
    CampaignPublisher:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        image:
          type: string
          nullable: true
      required:
        - id
        - name
        - email
    CampaignPoster:
      type: object
      properties:
        url:
          type: string
          description: Poster media URL.
        type:
          type: string
          enum: [image]
      required:
        - url
        - type
    CampaignInfo:
      type: object
      properties:
        description:
          type: string
        poster:
          $ref: '#/components/schemas/CampaignPoster'
          nullable: true
        operatingHours:
          type: string
        location:
          type: string
        ticketPrice:
          type: string
        subtitle:
          type: string
          nullable: true
      required:
        - description
        - operatingHours
        - location
        - ticketPrice
    CampaignParticipationMethod:
      type: object
      properties:
        description:
          type: string
        poster:
          $ref: '#/components/schemas/CampaignPoster'
          nullable: true
        notes:
          type: string
          nullable: true
        steps:
          type: array
          items:
            $ref: '#/components/schemas/CampaignParticipationStep'
          nullable: true
        keyNotes:
          type: array
          items:
            type: string
          nullable: true
      required:
        - description
    CampaignParticipationStep:
      type: object
      properties:
        title:
          type: string
        description:
          type: string
      required:
        - title
        - description
    CampaignFeaturedProduct:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        imageUrl:
          type: string
          nullable: true
        pointsCost:
          type: integer
          nullable: true
        priceCents:
          type: integer
          nullable: true
        currency:
          type: string
      required:
        - id
        - name
        - currency
    CampaignConfiguration:
      type: object
      properties:
        keywords:
          type: array
          items:
            type: string
        hashtags:
          type: array
          items:
            type: string
        redirectLinks:
          type: array
          items:
            $ref: '#/components/schemas/CampaignRedirectLink'
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/CampaignAttachment'
        imageCount:
          type: integer
        videoCount:
          type: integer
        rankMode:
          type: string
          enum: [TOP_PERFORMERS, ENGAGEMENT_BASED]
        scoreDistribution:
          type: object
          additionalProperties:
            type: number
      required:
        - keywords
        - hashtags
        - redirectLinks
        - attachments
        - imageCount
        - videoCount
        - rankMode
        - scoreDistribution
    CampaignAttachment:
      type: object
      properties:
        url:
          type: string
        type:
          type: string
          enum: [image, video]
      required:
        - url
        - type
    CampaignTimeline:
      type: object
      properties:
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        progress:
          type: integer
          description: Campaign progress percentage (0-100)
        daysRemaining:
          type: integer
          description: Number of days remaining until end date
      required:
        - startDate
        - endDate
        - createdAt
        - updatedAt
        - progress
        - daysRemaining
    CampaignRewards:
      type: object
      properties:
        totalPointsPool:
          type: integer
          description: Total points allocated for this campaign
        participationRewardPoints:
          type: integer
          description: Fixed points granted per participant for a submitted post
        totalPointsAwarded:
          type: integer
          description: Total points already awarded to contributors
        pointsRemaining:
          type: integer
          description: Points still available in the pool
      required:
        - totalPointsPool
        - totalPointsAwarded
        - pointsRemaining
    CampaignStatistics:
      type: object
      properties:
        totalContributors:
          type: integer
          description: Total number of unique contributors
        totalPosts:
          type: integer
          description: Total number of posts created
        totalViews:
          type: integer
          description: Total views across all posts
        totalEngagement:
          type: integer
          description: Total engagement (likes + comments + shares)
        totalLikes:
          type: integer
        totalComments:
          type: integer
        totalShares:
          type: integer
        averageEngagementPerPost:
          type: integer
          description: Average engagement per post
      required:
        - totalContributors
        - totalPosts
        - totalViews
        - totalEngagement
        - totalLikes
        - totalComments
        - totalShares
        - averageEngagementPerPost
    CampaignParticipation:
      type: object
      properties:
        total:
          type: integer
          description: Total number of participants
        accepted:
          type: integer
          description: Number of accepted participants
        rejected:
          type: integer
          description: Number of rejected participants
        completed:
          type: integer
          description: Number of completed participants
        dropped:
          type: integer
          description: Number of dropped participants
      required:
        - total
        - accepted
        - rejected
        - completed
        - dropped
    TopContributor:
      type: object
      properties:
        rank:
          type: integer
          description: Contributor's rank based on points earned
        userId:
          type: string
        userName:
          type: string
        userImage:
          type: string
          nullable: true
        postsCreated:
          type: integer
        pointsEarned:
          type: integer
        totalViews:
          type: integer
        totalEngagement:
          type: integer
        status:
          type: string
          enum: [ONGOING, COMPLETED, CANCELLED]
        joinedAt:
          type: string
          format: date-time
      required:
        - rank
        - userId
        - postsCreated
        - pointsEarned
        - totalViews
        - totalEngagement
        - status
        - joinedAt
    MarketingEmailAlignment:
      type: string
      enum: [left, center]
    MarketingEmailHeadingBlock:
      type: object
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [heading] }
        text: { type: string }
        level: { type: integer, enum: [1, 2, 3] }
        align: { $ref: '#/components/schemas/MarketingEmailAlignment' }
      required: [id, type, text, level, align]
    MarketingEmailTextBlock:
      type: object
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [text] }
        text:
          type: string
          description: Plain text. The only supported token is {{{contact.first_name|there}}}.
        align: { $ref: '#/components/schemas/MarketingEmailAlignment' }
      required: [id, type, text, align]
    MarketingEmailImageBlock:
      type: object
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [image] }
        src: { type: string, format: uri, pattern: '^https://' }
        alt: { type: string }
        linkUrl: { type: string, format: uri, pattern: '^https://' }
        width: { type: integer, minimum: 1, maximum: 640 }
      required: [id, type, src, alt]
    MarketingEmailButtonBlock:
      type: object
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [button] }
        label: { type: string }
        url: { type: string, format: uri, pattern: '^https://' }
        align: { $ref: '#/components/schemas/MarketingEmailAlignment' }
      required: [id, type, label, url, align]
    MarketingEmailDividerBlock:
      type: object
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [divider] }
      required: [id, type]
    MarketingEmailBlock:
      oneOf:
        - $ref: '#/components/schemas/MarketingEmailHeadingBlock'
        - $ref: '#/components/schemas/MarketingEmailTextBlock'
        - $ref: '#/components/schemas/MarketingEmailImageBlock'
        - $ref: '#/components/schemas/MarketingEmailButtonBlock'
        - $ref: '#/components/schemas/MarketingEmailDividerBlock'
      discriminator:
        propertyName: type
    MarketingEmailBlocksContent:
      type: object
      additionalProperties: false
      properties:
        contentMode: { type: string, enum: [BLOCKS] }
        blocks:
          type: array
          maxItems: 100
          items: { $ref: '#/components/schemas/MarketingEmailBlock' }
        htmlBody: { nullable: true, enum: [null] }
      required: [contentMode, blocks, htmlBody]
    MarketingEmailHtmlContent:
      type: object
      additionalProperties: false
      properties:
        contentMode: { type: string, enum: [HTML] }
        blocks:
          type: array
          maxItems: 0
          items: { $ref: '#/components/schemas/MarketingEmailBlock' }
        htmlBody:
          type: string
          maxLength: 50000
          description: Safe body fragment only; style, class, id, non-HTTPS URLs, and document tags are rejected.
      required: [contentMode, blocks, htmlBody]
    MarketingEmailBlocksDraftInput:
      type: object
      additionalProperties: false
      properties:
        name: { type: string, maxLength: 160 }
        subject:
          type: string
          maxLength: 200
          description: Plain text. The only supported token is {{{contact.first_name|there}}}.
        contentMode: { type: string, enum: [BLOCKS] }
        blocks:
          type: array
          maxItems: 100
          items: { $ref: '#/components/schemas/MarketingEmailBlock' }
        htmlBody: { nullable: true, enum: [null] }
      required: [name, subject, contentMode, blocks, htmlBody]
    MarketingEmailHtmlDraftInput:
      type: object
      additionalProperties: false
      properties:
        name: { type: string, maxLength: 160 }
        subject:
          type: string
          maxLength: 200
          description: Plain text. The only supported token is {{{contact.first_name|there}}}.
        contentMode: { type: string, enum: [HTML] }
        blocks:
          type: array
          maxItems: 0
          items: { $ref: '#/components/schemas/MarketingEmailBlock' }
        htmlBody:
          type: string
          maxLength: 50000
          description: Safe body fragment only; style, class, id, non-HTTPS URLs, and document tags are rejected.
      required: [name, subject, contentMode, blocks, htmlBody]
    MarketingEmailDraftInput:
      oneOf:
        - $ref: '#/components/schemas/MarketingEmailBlocksDraftInput'
        - $ref: '#/components/schemas/MarketingEmailHtmlDraftInput'
    MarketingEmailBlocksDraftSaveBody:
      type: object
      additionalProperties: false
      properties:
        name: { type: string, maxLength: 160 }
        subject: { type: string, maxLength: 200 }
        contentMode: { type: string, enum: [BLOCKS] }
        blocks:
          type: array
          maxItems: 100
          items: { $ref: '#/components/schemas/MarketingEmailBlock' }
        htmlBody: { nullable: true, enum: [null] }
        expectedRevision: { type: integer, minimum: 1 }
      required: [name, subject, contentMode, blocks, htmlBody, expectedRevision]
    MarketingEmailHtmlDraftSaveBody:
      type: object
      additionalProperties: false
      properties:
        name: { type: string, maxLength: 160 }
        subject: { type: string, maxLength: 200 }
        contentMode: { type: string, enum: [HTML] }
        blocks:
          type: array
          maxItems: 0
          items: { $ref: '#/components/schemas/MarketingEmailBlock' }
        htmlBody: { type: string, maxLength: 50000 }
        expectedRevision: { type: integer, minimum: 1 }
      required: [name, subject, contentMode, blocks, htmlBody, expectedRevision]
    MarketingEmailDraftSaveBody:
      oneOf:
        - $ref: '#/components/schemas/MarketingEmailBlocksDraftSaveBody'
        - $ref: '#/components/schemas/MarketingEmailHtmlDraftSaveBody'
    MarketingEmailDraft:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        subject: { type: string }
        contentMode: { type: string, enum: [BLOCKS, HTML] }
        blocks:
          type: array
          items: { $ref: '#/components/schemas/MarketingEmailBlock' }
        htmlBody: { type: string, nullable: true }
        revision: { type: integer }
        contentHash:
          type: string
          description: SHA-256 of the React Email canonical rendered snapshot.
        rendererVersion:
          type: string
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, name, subject, contentMode, blocks, revision, contentHash, rendererVersion, createdAt, updatedAt]
    MarketingEmailDraftDetail:
      allOf:
        - $ref: '#/components/schemas/MarketingEmailDraft'
        - type: object
          properties:
            testSend:
              $ref: '#/components/schemas/MarketingEmailTestSendReadiness'
          required: [testSend]
    MarketingEmailTestSendReadiness:
      type: object
      properties:
        canTest: { type: boolean }
        recipientMasked: { type: string, nullable: true }
        reason: { type: string, nullable: true }
      required: [canTest, recipientMasked, reason]
    MarketingEmailTestSendBody:
      type: object
      additionalProperties: false
      properties:
        expectedRevision: { type: integer, minimum: 1 }
        idempotencyKey: { type: string, format: uuid }
      required: [expectedRevision, idempotencyKey]
    MarketingEmailTestSendApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            status: { type: string, enum: [ACCEPTED] }
            recipientMasked: { type: string }
          required: [status, recipientMasked]
      required: [status, data]
    MarketingEmailDraftApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailDraft' }
      required: [status, data]
    MarketingEmailDraftDetailApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailDraftDetail' }
      required: [status, data]
    MarketingEmailDraftListApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: array
          items: { $ref: '#/components/schemas/MarketingEmailDraft' }
      required: [status, data]
    MarketingEmailPreviewApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: object
          properties:
            html: { type: string }
            contentHash: { type: string }
          required: [html, contentHash]
      required: [status, data]
    MarketingEmailEmptyBody:
      type: object
      additionalProperties: false
    MarketingEmailCreateSendRunBody:
      type: object
      additionalProperties: false
      properties:
        clientRunId:
          type: string
          format: uuid
          description: Stable client idempotency key. Reuse it after a lost response.
      required: [clientRunId]
    MarketingEmailConfirmBody:
      type: object
      additionalProperties: false
      properties:
        token: { type: string, format: uuid }
        clientConfirmationId:
          type: string
          format: uuid
          description: Stable confirmation idempotency key. Reuse it after a lost response.
      required: [token, clientConfirmationId]
    MarketingEmailSendRun:
      type: object
      properties:
        id: { type: string, format: uuid }
        draftId: { type: string, format: uuid }
        gateFingerprint: { type: string }
        status:
          type: string
          enum: [SNAPSHOT_PENDING, SYNC_PENDING, RECONCILE_PENDING, DRAFT_CREATE_UNKNOWN, CONFIRM_PENDING, CONFIRMED, SUBMIT_DELAYED, UNKNOWN, BLOCKED, ACCEPTED, FAILED]
        snapshotCount: { type: integer }
        snapshotHash: { type: string, nullable: true }
        snapshotConsentRevision: { type: integer, nullable: true }
        providerBroadcastId: { type: string, nullable: true }
        providerDraftName: { type: string }
        confirmationTokenExpiresAt: { type: string, format: date-time, nullable: true }
        confirmedAt: { type: string, format: date-time, nullable: true }
        submitDeadlineAt: { type: string, format: date-time, nullable: true }
        confirmedByUserId: { type: string, nullable: true }
        confirmationClientId: { type: string, format: uuid, nullable: true }
        terminalAt: { type: string, format: date-time, nullable: true }
        acceptedAt: { type: string, format: date-time, nullable: true }
      required: [id, draftId, gateFingerprint, status, snapshotCount, providerDraftName]
    MarketingEmailConfirmation:
      type: object
      properties:
        token: { type: string, format: uuid }
        expiresAt: { type: string, format: date-time }
      required: [token, expiresAt]
    MarketingEmailSendRunCreateData:
      type: object
      properties:
        runId: { type: string, format: uuid }
      required: [runId]
    MarketingEmailSendRunCreateApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailSendRunCreateData' }
      required: [status, data]
    MarketingEmailConfirmationApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailConfirmation' }
      required: [status, data]
    MarketingEmailSendRunApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailSendRun' }
      required: [status, data]
    MarketingEmailSendRunListApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data:
          type: array
          items: { $ref: '#/components/schemas/MarketingEmailSendRun' }
      required: [status, data]
    MarketingEmailSendRunDetailData:
      type: object
      properties:
        run: { $ref: '#/components/schemas/MarketingEmailSendRun' }
        preview:
          type: object
          nullable: true
          properties:
            subject: { type: string }
            html: { type: string }
          required: [subject, html]
      required: [run, preview]
    MarketingEmailSendRunDetailApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailSendRunDetailData' }
      required: [status, data]
    MarketingEmailWebhookReceipt:
      type: object
      properties:
        received: { type: boolean, enum: [true] }
        duplicate: { type: boolean }
      required: [received, duplicate]
    MarketingEmailWorkerResult:
      type: object
      properties:
        claimed: { type: integer }
        completed: { type: integer }
        submitted: { type: integer }
        jobsClaimed: { type: integer }
        purged: { type: integer }
        cleaned: { type: integer }
      required: [claimed, completed, submitted, jobsClaimed, purged, cleaned]
    MarketingEmailWorkerApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailWorkerResult' }
      required: [status, data]
    MarketingEmailWebhookHealth:
      type: object
      properties:
        receivedCount: { type: integer }
        latestReceivedAt: { type: string, format: date-time, nullable: true }
        latestOccurredAt: { type: string, format: date-time, nullable: true }
      required: [receivedCount, latestReceivedAt, latestOccurredAt]
    MarketingEmailWebhookHealthApiResponse:
      type: object
      properties:
        status: { type: string, enum: [success] }
        data: { $ref: '#/components/schemas/MarketingEmailWebhookHealth' }
      required: [status, data]
