openapi: 3.1.0
info:
  title: GNIZDO API
  version: 1.0.0
  description: |
    Minimal Laravel 12 API-only backend for GNIZDO multichannel e-commerce platform.

    ## Authentication

    Most endpoints require authentication via Laravel Sanctum. Include the token in the Authorization header:

    ```
    Authorization: Bearer {token}
    ```

    ## Error Format

    All errors wrap their payload in a consistent envelope so the SPA can rely on a single shape:

    ```json
    {
      "success": false,
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "Validation failed.",
        "details": {
          "email": ["Email is required."]
        },
        "status": 422
      }
    }
    ```

    Responses also return a server-generated `X-Request-Id` header for correlation.
    Client-provided `X-Request-Id` values are not trusted as the primary request ID.

servers:
  - url: https://gnizdo.ddev.site/api/v1
    description: Local development
  - url: http://localhost:8000/api/v1
    description: Direct Laravel server

paths:
  /waitlist:
    servers:
      - url: https://gnizdo.space/api
        description: Production landing API
      - url: http://localhost:8000/api
        description: Local Laravel server
    post:
      summary: Waitlist signup
      description: Collects beta access and inventory risk review requests from the public landing page.
      tags: [Landing]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WaitlistSignupRequest"
            examples:
              valid:
                summary: Valid email
                value:
                  email: store@example.com
                  platform: Shopify + Etsy
                  store_url: https://example-store.com
              inventory_risk_review:
                summary: Inventory risk review request
                value:
                  email: store@example.com
                  platform: Etsy + Shopify
                  request_type: inventory_risk_review
                  sku_count: "120"
                  recent_oversell_refund: "Oversold two limited-stock SKUs last month."
                  current_sync_tool: Manual spreadsheet
      responses:
        "200":
          description: Signup stored or already existed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WaitlistSignupResponse"
              examples:
                created:
                  summary: Signup stored
                  value:
                    ok: true
                already:
                  summary: Signup already exists
                  value:
                    ok: true
                    already: true
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WaitlistSignupValidationError"
              examples:
                invalid:
                  summary: Invalid email
                  value:
                    ok: false
                    message: Validation failed.
                    errors:
                      email:
                        - The email field must be a valid email address.
        "500":
          description: Server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WaitlistSignupErrorResponse"
              examples:
                error:
                  summary: Unexpected server error
                  value:
                    ok: false
                    message: We could not submit your request. Please try again.
                    code: WAITLIST_SUBMIT_FAILED
                    request_id: 018f3c6a-6c18-4a88-8a2e-91a6a8f9c111
  /auth/login:
    post:
      summary: User login
      description: Authenticate user with email and password. Returns user object and Sanctum token.
      tags: [Authentication]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
            examples:
              valid:
                summary: Valid credentials
                value:
                  email: admin@gnizdo.dev
                  password: Password1
      responses:
        "200":
          description: Successful login
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginResponse"
              examples:
                success:
                  summary: Successful login
                  value:
                    user:
                      id: 1
                      email: admin@gnizdo.dev
                      created_at: "2025-01-01T00:00:00.000000Z"
                      updated_at: "2025-01-01T00:00:00.000000Z"
                    token: "1|abcdef123456..."
                    token_type: Bearer
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalid_credentials:
                  summary: Invalid credentials
                  value:
                    success: false
                    error:
                      code: invalid_credentials
                      message: Invalid credentials
                      details: null
                      status: 401
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Too many requests (rate limit exceeded)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/google:
    post:
      summary: Google authentication
      description: Authenticate with a Google Identity Services credential. Returns user object and Sanctum token when social login is enabled.
      tags: [Authentication]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GoogleAuthRequest"
      responses:
        "200":
          description: Successful Google authentication
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginResponse"
        "401":
          description: Google credential verification failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Google authentication is forbidden for this account or beta access state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Social login endpoint is disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/register:
    post:
      summary: User registration
      description: |
        Creates or updates an email/password user and returns a Sanctum token.
        When a stored referral code captured from the first-party `gnizdo_ref` cookie is submitted, attribution is applied only if it resolves to another existing user.
        Social login is temporarily disabled. Public authentication currently uses email/password only.
      tags: [Authentication]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
      responses:
        "201":
          description: Account created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginResponse"
        "403":
          description: Not invited / not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/forgot-password:
    post:
      summary: Request password reset email
      description: |
        Sends a password reset link to the provided email (if an account exists).
        Always returns 200 to avoid leaking account existence.
      tags: [Authentication]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ForgotPasswordRequest"
      responses:
        "200":
          description: Reset email queued (or no-op)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ForgotPasswordResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/reset-password:
    post:
      summary: Reset password
      description: Resets user password using token from reset email.
      tags: [Authentication]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ResetPasswordRequest"
      responses:
        "200":
          description: Password reset successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResetPasswordResponse"
        "422":
          description: Invalid token / validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/logout:
    post:
      summary: User logout
      description: Revoke the current user's token.
      tags: [Authentication]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Successful logout
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Logged out

  /auth/me:
    get:
      summary: Get current user
      description: Returns the currently authenticated user's information.
      tags: [Authentication]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Current user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserResource"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/email/verification-notification:
    post:
      summary: Resend verification email
      description: |
        Sends a fresh email verification link for the current authenticated user when the account is still unverified.
        Already-verified users receive a graceful success response without sending another email.
      tags: [Authentication]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Verification email handled
          content:
            application/json:
              schema:
                type: object
                required: [sent, already_verified, message]
                properties:
                  sent:
                    type: boolean
                    example: true
                  already_verified:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Verification email sent.
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Too many resend attempts
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/impersonation/exit:
    post:
      summary: Exit an active impersonation session
      tags: [Authentication]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Impersonation session ended
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImpersonationExitResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: No active impersonation session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /settings/referrals:
    get:
      summary: Get referral settings for the authenticated user
      tags: [Settings]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Referral settings payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReferralSettingsResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/users:
    get:
      summary: List users for admin moderation and impersonation
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 1, maximum: 100 }
        - in: query
          name: search
          schema: { type: string }
          description: Case-insensitive search across user email and name. The authenticated superuser is excluded from both rows and total count even when the query matches their own email.
        - in: query
          name: status
          schema:
            type: string
            enum: [active, restricted, suspended]
        - in: query
          name: referral
          schema:
            type: string
            enum: [referred, not_referred]
      responses:
        "200":
          description: Paginated user list for superuser admin scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminUserListResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminAccessForbiddenResponse"

  /admin/support-events:
    get:
      summary: List recent support events for admin/support debugging
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: user_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: channel_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: sync_run_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Paginated support events
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminSupportEventsResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminAccessForbiddenResponse"

  /admin/waitlist-signups:
    get:
      summary: List waitlist signups for superusers
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 1, maximum: 100 }
        - in: query
          name: search
          schema: { type: string, maxLength: 255 }
          description: Case-insensitive search across email, platform, store URL, IP, and referrer.
        - in: query
          name: status
          schema:
            type: string
            enum: [pending, approved, invited, registered, rejected]
      responses:
        "200":
          description: Paginated waitlist signup list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminWaitlistSignupsResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminAccessForbiddenResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/waitlist-signups/{id}/approve:
    post:
      summary: Approve a waitlist signup
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AdminWaitlistSignupId"
      responses:
        "200":
          description: Waitlist signup approved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminWaitlistSignupResponse"
        "404":
          description: Waitlist signup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invalid waitlist transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/waitlist-signups/{id}/approve-invite:
    post:
      summary: Approve and invite a waitlist signup
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AdminWaitlistSignupId"
      responses:
        "200":
          description: Waitlist signup approved and invited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminWaitlistSignupResponse"
        "404":
          description: Waitlist signup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invalid waitlist transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/waitlist-signups/{id}/reject:
    post:
      summary: Reject a waitlist signup
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AdminWaitlistSignupId"
      responses:
        "200":
          description: Waitlist signup rejected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminWaitlistSignupResponse"
        "404":
          description: Waitlist signup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invalid waitlist transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/waitlist-signups/{id}/invite:
    post:
      summary: Invite an approved waitlist signup
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AdminWaitlistSignupId"
      responses:
        "200":
          description: Waitlist signup invited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminWaitlistSignupResponse"
        "404":
          description: Waitlist signup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invalid waitlist transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/waitlist-signups/{id}/resend-invite:
    post:
      summary: Resend a waitlist signup invite
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AdminWaitlistSignupId"
      responses:
        "200":
          description: Waitlist invite resent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminWaitlistSignupResponse"
        "404":
          description: Waitlist signup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invalid waitlist transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/incidents:
    get:
      summary: List linked operational incidents for admin investigation
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: type
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: severity
          schema: { type: string }
        - in: query
          name: channel_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: sync_run_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Paginated incidents
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminIncidentListResponse"

  /admin/incidents/{incidentId}:
    get:
      summary: Get incident summary for admin investigation
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: incidentId
          required: true
          schema: { type: integer, minimum: 1 }
      responses:
        "200":
          description: Incident detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminIncidentDetailResponse"

  /admin/incidents/{incidentId}/actions:
    post:
      summary: Apply a manual recovery action for one incident
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: incidentId
          required: true
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdminIncidentRecoveryActionRequest"
      responses:
        "200":
          description: Updated incident detail with refreshed diagnostics after the action executes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminIncidentDetailResponse"
        "404":
          description: Incident not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Recovery action blocked by canonical preconditions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Invalid recovery action payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/incidents/{incidentId}/timeline:
    get:
      summary: Get normalized event timeline for one incident
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: incidentId
          required: true
          schema: { type: integer, minimum: 1 }
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Incident timeline
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminTimelineResponse"

  /admin/timeline:
    get:
      summary: List normalized operational events across incidents
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: incident_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: channel_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: sync_run_id
          schema: { type: integer, minimum: 1 }
        - in: query
          name: severity
          schema: { type: string }
        - in: query
          name: kind
          schema: { type: string }
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Paginated normalized timeline events
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminTimelineResponse"

  /admin/users/{userId}:
    get:
      summary: Get read-only admin details for another user
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: userId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Admin user detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminUserDetailResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers or when targeting the authenticated superuser
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AdminAccessForbiddenResponse"
                  - $ref: "#/components/schemas/AdminSelfTargetForbiddenResponse"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/users/{userId}/impersonate:
    post:
      summary: Start impersonating another user
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: userId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Impersonation session created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminUserImpersonationResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers, self-targets, or protected targets
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AdminAccessForbiddenResponse"
                  - $ref: "#/components/schemas/AdminSelfTargetForbiddenResponse"
                  - $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/users/{userId}/status:
    patch:
      summary: Update user status for another user
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: userId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdminUserStatusUpdateRequest"
      responses:
        "200":
          description: Updated user status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminUserStatusUpdateResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers or when targeting the authenticated superuser
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AdminAccessForbiddenResponse"
                  - $ref: "#/components/schemas/AdminSelfTargetForbiddenResponse"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/users/{userId}/channels:
    get:
      summary: List another user's channels in read-only admin scope
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: userId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Sanitized target user channels
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminChannelsResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers or when targeting the authenticated superuser
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AdminAccessForbiddenResponse"
                  - $ref: "#/components/schemas/AdminSelfTargetForbiddenResponse"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/users/{userId}/stores:
    get:
      summary: List another user's stores in read-only admin scope
      description: Current beta projects stores from channel-backed cached data when no distinct store aggregate exists.
      tags: [Admin]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: userId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Sanitized target user stores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminStoresResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden for non-superusers or when targeting the authenticated superuser
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AdminAccessForbiddenResponse"
                  - $ref: "#/components/schemas/AdminSelfTargetForbiddenResponse"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/test-credentials:
    post:
      summary: Test channel credentials
      tags: [Channels]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [platform_code, credentials]
              properties:
                platform_code:
                  type: string
                  enum: [woo, shopify, etsy, ebay]
                credentials:
                  type: object
      responses:
        "200":
          description: Test result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      connection:
                        type: object
                        properties:
                          verified:
                            type: boolean
                          checked_at:
                            type: string
                          latency_ms:
                            type: number
                            nullable: true
                          reason:
                            type: string
                            nullable: true
        "400":
          description: Unsupported platform
        "422":
          description: Validation failed

  /channels:
    get:
      summary: List all channels
      description: Returns a list of all platform channels (stores renamed) for the authenticated user.
      tags: [Channels]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List of channels
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ChannelResource"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      summary: Create channel
      description: Create a new platform channel (legacy stores continue to map here).
      tags: [Channels]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChannelStoreRequest"
            examples:
              woocommerce:
                summary: WooCommerce channel
                value:
                  platform_code: woo
                  name: My WooCommerce Channel
                  credentials:
                    store_url: https://woo.gnizdo.ddev.site
                    consumer_key: ck_...
                    consumer_secret: cs_...
                  is_active: true
      responses:
        "201":
          description: Channel created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelDetailsResource"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: A channel for this platform already exists for the authenticated user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Channel creation is temporarily limited by anti-abuse controls
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /platforms:
    get:
      summary: List supported platform definitions
      description: Returns every configured platform along with the credential fields the SPA should render.
      tags: [Platforms]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Platform definitions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PlatformDefinition"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /help/limits:
    get:
      summary: Get operational usage limits
      description: Returns the main anti-abuse and operational limits shown in the Help page.
      tags: [Help]
      responses:
        "200":
          description: Help limits payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HelpLimitsResponse"

  /help/limits/runtime:
    get:
      summary: Get current runtime limits state
      description: Returns current user-facing runtime state for channel creation, optional channel-scoped sync cooldown, and account warm-up.
      tags: [Help]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: channel_id
          required: false
          schema:
            type: integer
            minimum: 1
          description: Optional owned channel ID for channel-scoped sync runtime state.
      responses:
        "200":
          description: Help runtime limits payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HelpLimitsRuntimeResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}:
    get:
      summary: Get channel details
      description: Returns details of a specific channel.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "200":
          description: Channel details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelDetailsResource"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    put:
      summary: Fully update channel
      description: Create or replace a channel by ID.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChannelStoreRequest"
      responses:
        "200":
          description: Channel updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelDetailsResource"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    patch:
      summary: Partially update channel
      description: Update a subset of channel attributes.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateChannelRequest"
      responses:
        "200":
          description: Channel updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelDetailsResource"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      summary: Delete channel
      description: Removes a channel connection.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "204":
          description: Channel deleted
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /onboarding/status:
    get:
      summary: Onboarding status
      description: Returns derived onboarding state and first sync status. Canonical trusted channel sync history takes precedence over stale orphaned `initial_sync_runs`, which are reconciled before Phase B routing is derived.
      tags: [Onboarding]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Onboarding status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OnboardingStatusResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /sync/first/run:
    post:
      summary: Start first sync
      description: Starts (or returns) an idempotent first sync run. Stale orphaned active runs are first reconciled to a terminal state, then repeated recovery requests reuse the remaining active or same-day successful run instead of applying request cooldowns.
      tags: [Onboarding]
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FirstSyncRunRequest"
      responses:
        "202":
          description: First sync accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FirstSyncRunResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /sync/first/runs/{runId}:
    get:
      summary: First sync run status
      description: Returns details for a first sync run.
      tags: [Onboarding]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: runId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: First sync run
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FirstSyncRunStatusResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /daily-check/latest:
    get:
      tags: [Onboarding]
      summary: Get latest daily check report JSON
      description: Returns the latest `daily_reports.report_json` for the authenticated user.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Latest daily report (nullable if none exists)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    allOf:
                      - $ref: "#/components/schemas/DailyReportJson"
                    nullable: true
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /daily-check/{dailyRunId}/events:
    get:
      tags: [Onboarding]
      summary: Get daily report events
      description: Returns paged `daily_events` for a daily run.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: dailyRunId
          required: true
          schema:
            type: string
            format: uuid
        - in: query
          name: page
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: per_page
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Daily report events
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/DailyEvent"
                  meta:
                    $ref: "#/components/schemas/PaginationMeta"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{channel}/connect:
    post:
      summary: Connect channel
      description: Connects a channel (WooCommerce in MVP).
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: channel
          required: true
          schema:
            type: string
            enum: [woocommerce, shopify, etsy, ebay]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChannelConnectRequest"
      responses:
        "200":
          description: Connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelConnectResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Not implemented
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{channel}/callback:
    get:
      summary: OAuth callback (reserved)
      description: Reserved for OAuth-based channels.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: channel
          required: true
          schema:
            type: string
            enum: [shopify, etsy, ebay]
      responses:
        "501":
          description: Not implemented
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{channel}/disconnect:
    post:
      summary: Disconnect channel
      description: Revokes a channel connection.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: channel
          required: true
          schema:
            type: string
            enum: [woocommerce, shopify, etsy, ebay]
      responses:
        "204":
          description: Disconnected
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Not implemented
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/connection/test:
    post:
      summary: Test channel connection
      description: Validates the stored credentials for the channel.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "200":
          description: Connection succeeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  connected:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Connection successful.
        "400":
          description: Connection failed (invalid credentials / platform error)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/check-connection:
    get:
      summary: Check channel connection
      description: Performs a pure connection check and updates only connection fields on the channel snapshot.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "200":
          description: Connection check result
          content:
            application/json:
              schema:
                type: object
                required: [status, checked_at]
                properties:
                  status:
                    type: string
                    enum: [ok, error]
                    example: ok
                  reason:
                    type: string
                    nullable: true
                    example: Invalid platform API key or secret.
                  checked_at:
                    type: string
                    format: date-time
        "400":
          description: Unsupported platform
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/issue-report:
    post:
      summary: Report channel issue
      description: Stores a support-facing issue report with sanitized channel diagnostics.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChannelIssueReportRequest"
      responses:
        "202":
          description: Issue report accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelIssueReportResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Too many issue reports
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/status:
    get:
      summary: Channel status
      description: Returns the computed WooCommerce channel state, connection info, stats, and sync progress.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "200":
          description: Channel status payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelStatusResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/sync:
    post:
      summary: Start or retry a channel sync run
      description: Triggers a sync run for the channel. True bootstrap first sync starts use a softer limiter than repeat manual re-syncs.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "202":
          description: Sync run accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelSyncResponse"
        "400":
          description: Unsupported platform for sync
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Sync already pending or running for this channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Repeated manual sync spam without progress is temporarily limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/first-sync:
    post:
      summary: Start a channel bootstrap sync run
      description: Starts the first sync path for a channel. Bootstrap and failed-run recovery attempts bypass coarse cooldowns; only repeated established re-sync spam remains rate limited.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "202":
          description: Sync run accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelSyncResponse"
        "400":
          description: Unsupported platform for sync
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Sync already pending or running for this channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Repeated manual sync spam without progress is temporarily limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/sync-runs:
    get:
      summary: List channel sync runs
      description: Returns paginated sync runs for a channel.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: Channel ID
        - in: query
          name: page
          required: false
          schema:
            type: integer
            minimum: 1
          description: Page number (1-based).
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
          description: Items per page (default 15).
      responses:
        "200":
          description: Paginated sync runs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelSyncRunListResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/store-profile:
    get:
      summary: Get store profile
      description: Returns the cached store profile for the channel along with the latest sync run (if any).
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "200":
          description: Store profile payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreProfileResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{id}/after-sync:
    get:
      summary: Get after-sync summary
      description: >
        Returns the current after-sync summary for a channel. If the cached store
        profile is stale, stale-profile detection may enqueue an asynchronous
        refresh job as a non-blocking side effect before the response is returned.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "200":
          description: After-sync summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AfterSyncResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /sync-runs/{id}:
    get:
      summary: Sync run detail
      description: Returns the sync run record along with its computed stage progress.
      tags: [Channels]
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: Sync run ID
      responses:
        "200":
          description: Sync run detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyncRunDetailResponse"
        "401":
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Sync run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{channel}/catalog:
    get:
      summary: Get store catalog
      description: >
        Returns paginated root catalog products. Each row includes `children`
        that expose the matching variations for the parent product.
      tags: [Catalog]
      security:
        - bearerAuth: []
      parameters:
        - name: channel
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
          description: Page number, defaults to 1
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
          description: Page size, defaults to 15
        - name: type
          in: query
          schema:
            type: string
            enum: [simple, variable, variation, grouped]
          description: Filter by product type
        - name: sku
          in: query
          schema:
            type: string
            maxLength: 64
          description: Partial SKU match
        - name: status
          in: query
          schema:
            type: string
            enum: [publish, draft, private, pending]
          description: Filter by status
        - name: stock_status
          in: query
          schema:
            type: string
            enum: [in_stock, out_of_stock, on_backorder]
          description: Filter by stock_state
        - name: search
          in: query
          schema:
            type: string
            maxLength: 255
          description: Search across name and SKU
      responses:
        "200":
          description: Catalog tree
          content:
            application/json:
              schema:
                type: object
                required:
                  [
                    data,
                    meta,
                    missing_price_count,
                    priced_from_variants_count,
                    stock_not_tracked_count,
                    stock_unknown_count,
                  ]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WooProductResource"
                  meta:
                    type: object
                    required:
                      [current_page, per_page, total, items_total, last_page]
                    properties:
                      current_page:
                        type: integer
                      per_page:
                        type: integer
                      total:
                        type: integer
                        description: Root catalog product count for the channel.
                      items_total:
                        type: integer
                        description: Total synced catalog item count for the channel, including variations.
                      last_page:
                        type: integer
                  missing_price_count:
                    type: integer
                    description: Count of catalog rows missing a price signal.
                  priced_from_variants_count:
                    type: integer
                    description: Count of parent rows priced from variants.
                  stock_not_tracked_count:
                    type: integer
                    description: Count of rows where stock is not tracked.
                  stock_unknown_count:
                    type: integer
                    description: Count of rows with unknown stock status.
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{channel}/catalog/sync:
    post:
      summary: Sync store catalog
      description: Triggers an inline synchronization against the connected platform.
      tags: [Catalog]
      security:
        - bearerAuth: []
      parameters:
        - name: channel
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
      responses:
        "200":
          description: Sync completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogSyncResponse"
        "404":
          description: Channel not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Catalog sync failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{channel}/catalog/{product}:
    get:
      summary: Get catalog product details
      description: Returns a single catalog product or variation for the provided channel.
      tags: [Catalog]
      security:
        - bearerAuth: []
      parameters:
        - name: channel
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
        - name: product
          in: path
          required: true
          schema:
            type: integer
          description: Catalog product ID
      responses:
        "200":
          description: Catalog product
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WooProductResource"
        "404":
          description: Channel or product not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /channels/{channel}/items/{product}:
    patch:
      summary: Update catalog item pricing and stock
      description: Inline price and inventory updates for WooCommerce products or variations; variation quantity edits infer availability automatically, and providing `stock_status` for a parent cascades the new availability to every child.
      tags: [Catalog]
      security:
        - bearerAuth: []
      parameters:
        - name: channel
          in: path
          required: true
          schema:
            type: integer
          description: Channel ID
        - name: product
          in: path
          required: true
          schema:
            type: integer
          description: Catalog item ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CatalogProductItemRequest"
      responses:
        "200":
          description: Updated catalog product
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WooProductResource"
        "404":
          description: Channel or product not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Channel write capability required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Sync failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /health:
    get:
      summary: Health check
      description: Check API and database health status.
      tags: [System]
      responses:
        "200":
          description: System healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  db:
                    type: string
                    example: ok
                  timestamp:
                    type: string
                    format: date-time
                    example: "2025-01-01T00:00:00.000000Z"
        "503":
          description: Database connection failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                db_error:
                  summary: Database connection failed
                  value:
                    success: false
                    error:
                      code: DB_CONNECTION_FAILED
                      message: Database connection failed.
                      details: null
                      status: 503

  /version:
    get:
      summary: Version information
      description: Returns application version and environment information.
      tags: [System]
      responses:
        "200":
          description: Version information
          content:
            application/json:
              schema:
                type: object
                properties:
                  version:
                    type: string
                    example: "1.0.0"
                  env:
                    type: string
                    example: local
                  commit:
                    type: string
                    nullable: true
                    example: null
                  build:
                    type: string
                    nullable: true
                    example: null

  /system/logs:
    get:
      summary: Latest system logs
      description: Returns paginated log entries from system, UI, jobs, or audit channels with filtering and search.
      tags: [System]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: channel
          schema:
            type: string
            enum: [ui, jobs, audit, system]
          description: Target log channel (default system).
        - in: query
          name: level
          schema:
            type: string
            enum: [INFO, WARNING, ERROR, CRITICAL, EMERGENCY, ALL]
          description: Optional level filter; ALL or empty disables filtering.
        - in: query
          name: channel_id
          schema:
            type: integer
          description: Filter logs by channel_id context when present.
        - in: query
          name: run_id
          schema:
            type: integer
          description: Filter logs by sync run id (context sync_run_id or run_id).
        - in: query
          name: platform
          schema:
            type: string
            maxLength: 64
          description: Filter logs by platform context when present.
        - in: query
          name: log_ref
          schema:
            type: string
            maxLength: 128
          description: Filter logs by log_ref context when present.
        - in: query
          name: request_id
          schema:
            type: string
            maxLength: 64
          description: Filter logs by request_id when present in context.
        - in: query
          name: search
          schema:
            type: string
            maxLength: 255
          description: Case-insensitive search across message, raw line, and context/extra.
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number (1-indexed).
        - in: query
          name: per_page
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Items per page.
      responses:
        "200":
          description: Recent logs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SystemLogsResponse"

  /openapi.yaml:
    get:
      summary: OpenAPI specification
      description: Returns the OpenAPI specification in YAML format.
      tags: [Documentation]
      responses:
        "200":
          description: OpenAPI specification
          content:
            application/yaml:
              schema:
                type: string

  /docs/tree:
    get:
      summary: Documentation tree
      description: Returns the documentation file tree.
      tags: [Documentation]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Documentation tree

  /docs/file:
    get:
      summary: Documentation file
      description: Returns a specific documentation file.
      tags: [Documentation]
      security:
        - bearerAuth: []
      parameters:
        - name: path
          in: query
          required: true
          schema:
            type: string
          description: File path
      responses:
        "200":
          description: Documentation file content

  /docs/db-schema:
    get:
      summary: Database schema
      description: Returns the current database schema.
      tags: [Documentation]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Database schema

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sanctum token
      description: |
        Laravel Sanctum token obtained from `/auth/login`.

        Include in Authorization header:
        ```
        Authorization: Bearer {token}
        ```

  parameters:
    AdminWaitlistSignupId:
      name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
      description: Waitlist signup ID

  schemas:
    ErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
          description: Always `false` for failed responses
        error:
          type: object
          required:
            - code
            - message
            - status
          properties:
            code:
              type: string
              example: VALIDATION_ERROR
              description: Machine-readable error code
            message:
              type: string
              example: The given data was invalid.
              description: Human-readable summary of the problem
            details:
              type: object
              nullable: true
              description: Field-specific issues (usually available for validation errors). Selected user-facing failures may also include safe support metadata such as `log_ref` and `retry_after`.
              properties:
                log_ref:
                  type: string
                  nullable: true
                  description: Safe support reference that can be shared with support to find the matching backend log entry.
                  example: channel:8f2c1a7b
                retry_after:
                  type: integer
                  nullable: true
                  description: Suggested number of seconds to wait before retrying a rate-limited action.
                  example: 45
              additionalProperties:
                description: Field-level issue arrays or single strings.
                oneOf:
                  - type: array
                    items:
                      type: string
                  - type: string
              example:
                email:
                  - The email field is required.
            status:
              type: integer
              example: 422
              description: HTTP status code returned by the endpoint

    AdminAccessForbiddenResponse:
      allOf:
        - $ref: "#/components/schemas/ErrorResponse"
        - type: object
          properties:
            error:
              type: object
              required: [code, message, status]
              properties:
                code:
                  type: string
                  const: AUTH_FORBIDDEN
                  example: AUTH_FORBIDDEN
                message:
                  type: string
                  example: Forbidden.
                details:
                  type: object
                  nullable: true
                  additionalProperties: true
                status:
                  type: integer
                  const: 403
                  example: 403

    AdminSelfTargetForbiddenResponse:
      allOf:
        - $ref: "#/components/schemas/ErrorResponse"
        - type: object
          properties:
            error:
              type: object
              required: [code, message, status]
              properties:
                code:
                  type: string
                  const: ADMIN_SELF_TARGET_FORBIDDEN
                  example: ADMIN_SELF_TARGET_FORBIDDEN
                message:
                  type: string
                  example: Superusers cannot inspect themselves via admin endpoints.
                details:
                  type: object
                  nullable: true
                  additionalProperties: true
                status:
                  type: integer
                  const: 403
                  example: 403

    WaitlistSignupRequest:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          format: email
          maxLength: 255
          example: store@example.com
          description: Email address for waitlist access
        website:
          type: string
          nullable: true
          description: Anti-spam honeypot, must be empty.
        platform:
          type: string
          nullable: true
          maxLength: 255
          example: Shopify + Etsy
          description: Primary platform(s) used by the requesting store.
        store_url:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
          example: https://example-store.com
          description: Optional storefront URL to help us vet the request.
        request_type:
          type: string
          enum: [beta_access, inventory_risk_review]
          default: beta_access
          description: Controlled request type submitted by the landing form.
        sku_count:
          type: string
          nullable: true
          maxLength: 120
          example: "120"
          description: Approximate SKU count for inventory risk review qualification.
        recent_oversell_refund:
          type: string
          nullable: true
          maxLength: 500
          example: "Oversold two limited-stock SKUs last month."
          description: Recent oversell, refund, or related inventory incident described by the requester.
        current_sync_tool:
          type: string
          nullable: true
          maxLength: 255
          example: Manual spreadsheet
          description: Current sync tool or manual process used by the requester.

    WaitlistSignupResponse:
      type: object
      required:
        - ok
      properties:
        ok:
          type: boolean
          example: true
        already:
          type: boolean
          nullable: true
          description: Present when the email already exists in the waitlist

    WaitlistSignupValidationError:
      type: object
      required:
        - ok
        - message
        - errors
      properties:
        ok:
          type: boolean
          example: false
        message:
          type: string
          example: Validation failed.
        errors:
          type: object
          properties:
            email:
              type: array
              items:
                type: string
              example:
                - The email field must be a valid email address.

    WaitlistSignupErrorResponse:
      type: object
      required:
        - ok
        - message
        - code
      properties:
        ok:
          type: boolean
          example: false
        message:
          type: string
          description: Public-safe error message.
          example: We could not submit your request. Please try again.
        code:
          type: string
          description: Machine-readable public waitlist failure code.
          example: WAITLIST_SUBMIT_FAILED
        request_id:
          type: string
          nullable: true
          description: Safe request correlation id when available.
          example: 018f3c6a-6c18-4a88-8a2e-91a6a8f9c111

    SystemLogEntry:
      type: object
      description: Single log entry aggregated from system log lines.
      properties:
        id:
          type: string
          description: Stable identifier derived from the raw line.
        datetime:
          type: string
          format: date-time
          nullable: true
          description: Timestamp recorded in the log line.
        message:
          type: string
          description: Log message.
        level:
          type: string
          nullable: true
          description: Monolog level name.
          example: ERROR
        level_name:
          type: string
          nullable: true
          description: Monolog level name (alias).
        channel:
          type: string
          nullable: true
          description: Monolog channel name.
        context:
          type: object
          additionalProperties: true
          description: Context payload for the log entry.
        extra:
          type: object
          additionalProperties: true
          description: Extra payload for the log entry.
        stacktrace:
          type: array
          description: Aggregated stacktrace lines for the log entry.
          items:
            type: string
        raw:
          type: string
          description: Original log line for the entry.
          example: "[2024-01-01 00:00:00] laravel.ERROR: Example failure"
        log_ref:
          type: string
          nullable: true
          description: Optional log reference carried in context or extra.

    SystemLogsResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [items]
          properties:
            items:
              type: array
              description: Log entries, newest first.
              items:
                $ref: "#/components/schemas/SystemLogEntry"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

    AdminSupportEvent:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
          nullable: true
        channel_id:
          type: integer
          nullable: true
        sync_run_id:
          type: integer
          nullable: true
        type:
          type: string
          enum:
            - channel_created
            - channel_updated
            - channel_deleted
            - connection_check_completed
            - connection_check_failed
            - sync_accepted
            - sync_completed
            - sync_failed
            - permission_denied
            - admin_access_granted
            - user_status_updated
            - admin_user_impersonation_started
            - admin_user_impersonation_ended
            - password_reset_requested
            - channel_issue_reported
            - sync_stale_recovery_applied
            - sync_manual_recovery_applied
        status:
          type: string
          enum: [accepted, completed, failed, blocked, partial]
        source:
          type: string
          nullable: true
          enum: [ui, job, audit, system]
        message:
          type: string
        log_ref:
          type: string
          nullable: true
        metadata:
          type: object
          nullable: true
          additionalProperties: true
        created_at:
          type: string
          format: date-time

    AdminSupportEventsResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [items]
          properties:
            items:
              type: array
              items:
                $ref: "#/components/schemas/AdminSupportEvent"
        meta:
          type: object
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer

    AdminIncident:
      type: object
      required:
        [
          id,
          incident_uuid,
          type,
          status,
          severity,
          affected_entities,
          event_count,
          related_incidents,
        ]
      properties:
        id:
          type: integer
        incident_uuid:
          type: string
          format: uuid
        type:
          type: string
          description: Deterministic incident classification derived from normalized event kinds.
        status:
          type: string
          description: Deterministic operational state derived from the latest linked event.
        severity:
          type: string
          description: Highest severity seen across linked events.
        first_event_at:
          type: string
          format: date-time
          nullable: true
        last_event_at:
          type: string
          format: date-time
          nullable: true
        channel_id:
          type: integer
          nullable: true
        sync_run_id:
          type: integer
          nullable: true
        order_id:
          type: string
          nullable: true
        sku_id:
          type: string
          nullable: true
        probable_root_cause:
          type: string
          nullable: true
          description: Deterministic heuristic to help operator triage. Not an authoritative diagnosis.
        affected_entities:
          type: object
          required: [user_ids, channel_ids, sync_run_ids, order_ids, sku_ids]
          properties:
            user_ids:
              type: array
              items:
                type: integer
            channel_ids:
              type: array
              items:
                type: integer
            sync_run_ids:
              type: array
              items:
                type: integer
            order_ids:
              type: array
              items:
                type: string
            sku_ids:
              type: array
              items:
                type: string
        event_count:
          type: integer
        latest_message:
          type: string
          nullable: true
        pattern:
          type: object
          nullable: true
          required: [id, pattern_key, seen_count, is_recurring]
          properties:
            id:
              type: integer
            pattern_key:
              type: string
            seen_count:
              type: integer
            first_seen_at:
              type: string
              format: date-time
              nullable: true
            last_seen_at:
              type: string
              format: date-time
              nullable: true
            is_recurring:
              type: boolean
        related_incidents:
          type: array
          items:
            type: object
            required: [id, type, status, severity]
            properties:
              id:
                type: integer
              type:
                type: string
              status:
                type: string
              severity:
                type: string
              last_event_at:
                type: string
                format: date-time
                nullable: true
        available_actions:
          $ref: "#/components/schemas/AdminIncidentAvailableActions"
        diagnostics:
          $ref: "#/components/schemas/AdminIncidentDiagnostics"

    AdminIncidentAvailableActions:
      type: object
      required:
        [
          can_retry_last_failed_sync,
          can_clear_requires_reauth,
          can_dismiss_stale_pending,
        ]
      properties:
        can_retry_last_failed_sync:
          type: boolean
        can_clear_requires_reauth:
          type: boolean
        can_dismiss_stale_pending:
          type: boolean

    AdminIncidentDiagnostics:
      type: object
      required: [channel, sync_run, latest_sync_run]
      properties:
        channel:
          allOf:
            - $ref: "#/components/schemas/AdminChannel"
          nullable: true
        sync_run:
          allOf:
            - $ref: "#/components/schemas/ChannelSyncRunResource"
          nullable: true
        latest_sync_run:
          allOf:
            - $ref: "#/components/schemas/ChannelSyncRunResource"
          nullable: true

    AdminIncidentRecoveryActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum:
            - retry_last_failed_sync
            - clear_requires_reauth
            - dismiss_stale_pending

    AdminTimelineEvent:
      type: object
      required: [id, event_uuid, kind, severity, title]
      properties:
        id:
          type: integer
        event_uuid:
          type: string
          format: uuid
        kind:
          type: string
        status:
          type: string
          nullable: true
        severity:
          type: string
        title:
          type: string
        message:
          type: string
          nullable: true
        fingerprint:
          type: string
          nullable: true
        user_id:
          type: integer
          nullable: true
        channel_id:
          type: integer
          nullable: true
        sync_run_id:
          type: integer
          nullable: true
        order_id:
          type: string
          nullable: true
        sku_id:
          type: string
          nullable: true
        source_table:
          type: string
          nullable: true
        source_id:
          type: string
          nullable: true
        occurred_at:
          type: string
          format: date-time
          nullable: true
        context:
          type: object
          nullable: true
          additionalProperties: true
        incident:
          type: object
          nullable: true
          required: [id, type, status, severity]
          properties:
            id:
              type: integer
            type:
              type: string
            status:
              type: string
            severity:
              type: string

    AdminIncidentListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
        data:
          type: object
          required: [items]
          properties:
            items:
              type: array
              items:
                $ref: "#/components/schemas/AdminIncident"
        meta:
          type: object
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer

    AdminIncidentDetailResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
        data:
          $ref: "#/components/schemas/AdminIncident"

    AdminTimelineResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
        data:
          type: object
          required: [items]
          properties:
            items:
              type: array
              items:
                $ref: "#/components/schemas/AdminTimelineEvent"
        meta:
          type: object
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer

    PaginationMeta:
      type: object
      properties:
        current_page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
        last_page:
          type: integer

    LoginRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          format: email
          example: admin@gnizdo.dev
          description: User email address
        password:
          type: string
          format: password
          example: Password1
          description: User password

    GoogleAuthRequest:
      type: object
      required:
        - credential
      properties:
        credential:
          type: string
          minLength: 20
          description: Google Identity Services credential token.

    RegisterRequest:
      type: object
      required:
        - email
        - password
        - password_confirmation
      properties:
        email:
          type: string
          format: email
          maxLength: 255
          example: store@example.com
        password:
          type: string
          format: password
          minLength: 8
          example: Password1
        password_confirmation:
          type: string
          format: password
          example: Password1
        referral_code:
          type: string
          nullable: true
          minLength: 8
          maxLength: 8
          pattern: "^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$"
          example: 7K4X2P7Z
          description: Optional referral code captured client-side from the canonical public invite route and forwarded during signup.

    ForgotPasswordRequest:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          format: email
          maxLength: 255
          example: store@example.com

    ForgotPasswordResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [sent, message]
          properties:
            sent:
              type: boolean
              example: true
            message:
              type: string
              example: If an account exists for this email, a reset link has been sent.

    ResetPasswordRequest:
      type: object
      required:
        - email
        - token
        - password
        - password_confirmation
      properties:
        email:
          type: string
          format: email
        token:
          type: string
          description: Password reset token from email link
        password:
          type: string
          format: password
          minLength: 8
        password_confirmation:
          type: string
          format: password

    ResetPasswordResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [message]
          properties:
            message:
              type: string
              example: Password has been reset.

    LoginResponse:
      type: object
      required:
        - user
        - token
        - token_type
      properties:
        user:
          $ref: "#/components/schemas/UserResource"
        token:
          type: string
          example: "1|abcdef123456..."
          description: Sanctum bearer token
        token_type:
          type: string
          example: Bearer
          description: Token type (always "Bearer")

    UserResource:
      type: object
      required:
        - id
        - email
        - is_superuser
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          example: 00000000-0000-0000-0000-000000000001
          description: Canonical user UUID
        name:
          type: string
          nullable: true
          example: Ivan Petrenko
          description: Display name when available.
        email:
          type: string
          format: email
          example: admin@gnizdo.dev
          description: User email address
        is_superuser:
          type: boolean
          example: false
          description: Superuser flag used to unlock admin routes.
        status:
          type: string
          enum: [active, restricted, suspended]
          example: active
        impersonation:
          $ref: "#/components/schemas/ImpersonationContext"
          nullable: true
        signup_ip:
          type: string
          nullable: true
          example: 203.0.113.10
          description: First observed signup IP, used for lightweight anti-abuse review.
        email_verified_at:
          type: string
          format: date-time
          nullable: true
          description: Application-level email verification timestamp.
        created_at:
          type: string
          format: date-time
          example: "2025-01-01T00:00:00.000000Z"
          description: Account creation timestamp
        updated_at:
          type: string
          format: date-time
          example: "2025-01-01T00:00:00.000000Z"
          description: Last update timestamp
    ImpersonationContext:
      type: object
      required: [active]
      properties:
        active:
          type: boolean
          example: true
        admin_id:
          type: string
          format: uuid
          nullable: true
        admin_email:
          type: string
          format: email
          nullable: true

    ReferralInvitedUser:
      type: object
      required: [id, email, status]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        joined_at:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum: [active, restricted, suspended]

    ReferralSettingsResponse:
      type: object
      required: [referral_link, invited_users]
      properties:
        referral_link:
          type: string
          format: uri
          example: https://gnizdo.space/nest/7K4X2P7Z
        invited_users:
          type: array
          items:
            $ref: "#/components/schemas/ReferralInvitedUser"

    ChannelResource:
      type: object
      required:
        - id
        - platform_code
        - name
        - status
        - is_active
        - created_at
        - updated_at
      properties:
        id:
          type: integer
          example: 1
          description: Channel ID
        platform_code:
          type: string
          enum: [woo, etsy, shopify]
          example: woo
          description: Platform identifier (WooCommerce only; other codes are reserved for future beta work.)
        name:
          type: string
          example: My WooCommerce Channel
          description: Channel display name
        capabilities:
          type: object
          readOnly: true
          description: Channel capabilities derived from platform and settings
          properties:
            read:
              type: boolean
              example: true
            write:
              type: boolean
              example: true
          required:
            - read
            - write
        store_url:
          type: string
          nullable: true
          example: https://shop.test
          description: Storefront URL resolved from platform metadata
          readOnly: true
        currency:
          type: string
          nullable: true
          example: USD
          description: Store currency code
          readOnly: true
        country:
          type: string
          nullable: true
          example: US
          description: Country for the store base location
          readOnly: true
        region:
          type: string
          nullable: true
          example: CA
          description: Region/state extracted from the base location
          readOnly: true
        timezone:
          type: string
          nullable: true
          example: America/Chicago
          description: Store timezone, when provided by the platform
          readOnly: true
        locale:
          type: string
          nullable: true
          example: en_US
          description: Store locale or language code
          readOnly: true
        platform_version:
          type: string
          nullable: true
          example: 8.0.2
          description: Platform (WooCommerce) version reported by the store
          readOnly: true
        price_decimals:
          type: integer
          nullable: true
          example: 2
          description: Number of decimal places used for prices
          readOnly: true
        weight_unit:
          type: string
          nullable: true
          example: kg
          description: Weight unit configured on the store
          readOnly: true
        dimension_unit:
          type: string
          nullable: true
          example: cm
          description: Dimension unit configured on the store
          readOnly: true
        tax_included:
          type: boolean
          nullable: true
          description: Whether prices include tax
          example: true
          readOnly: true
        connected_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T00:10:00.000000Z"
          description: Timestamp of the last successful connection handshake
          readOnly: true
        status:
          type: string
          enum:
            [
              connected,
              sync_running,
              sync_failed,
              connection_lost,
              needs_attention,
            ]
          example: connected
          description: Derived channel health state for the admin UI
          readOnly: true
        health_state:
          type: string
          enum:
            [
              connected,
              sync_running,
              sync_failed,
              connection_lost,
              needs_attention,
            ]
          example: connected
          description: Explicit alias of the derived channel health state
          readOnly: true
        active:
          type: boolean
          example: true
          description: Read-only alias of `is_active`
          readOnly: true
        connection_status:
          type: string
          enum: [disconnected, oauth_pending, connected, revoked, error]
          example: connected
          description: Canonical connection status for onboarding and sync flows
          readOnly: true
        is_active:
          type: boolean
          example: true
          description: Whether the channel is active
        last_connection_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T00:00:00.000000Z"
          description: Last successful connection timestamp
          readOnly: true
        last_connection_check_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T00:05:00.000000Z"
          description: Timestamp of the most recent connection check
          readOnly: true
        last_connection_check_status:
          type: string
          enum: [succeeded, failed]
          nullable: true
          example: succeeded
          description: Outcome of the most recent connection check
          readOnly: true
        last_connection_error:
          type: string
          nullable: true
          example: Invalid credentials
          description: Safe connection error message (no secrets)
          readOnly: true
        last_successful_sync_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T11:00:00Z"
          description: Last successful catalog sync timestamp
          readOnly: true
        last_sync_attempt_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T12:00:00Z"
          description: Most recent sync attempt timestamp
          readOnly: true
        sync_status:
          type: string
          enum: [running, success, failed, partial]
          example: running
          description: Current sync status derived from the latest run or last completion
          readOnly: true
        last_synced_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T11:00:00Z"
          description: Last successful catalog sync timestamp
          readOnly: true
        last_sync_status:
          type: string
          enum: [success, partial, failed]
          nullable: true
          example: success
          description: Outcome of the most recent completed sync attempt
          readOnly: true
        last_sync_error:
          type: string
          nullable: true
          example: Sync failed
          description: Safe sync error message (no secrets)
          readOnly: true
        products_synced_count:
          type: integer
          nullable: true
          example: 42
          description: Trusted synced catalog count from the last successful sync
          readOnly: true
        is_stale:
          type: boolean
          example: false
          description: Whether the last successful sync is older than the freshness window
          readOnly: true
        requires_reauth:
          type: boolean
          example: false
          description: Whether the latest canonical failed sync diagnostics require reauthorization to recover
          readOnly: true
        created_at:
          type: string
          format: date-time
          example: "2025-01-01T00:00:00.000000Z"
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          example: "2025-01-01T00:00:00.000000Z"
          description: Last update timestamp

    ChannelDetailsResource:
      allOf:
        - $ref: "#/components/schemas/ChannelResource"
        - type: object
          properties:
            credentials:
              type: object
              description: Platform credentials exposed for admin workflows.
              additionalProperties: true
              properties:
                store_url:
                  type: string
                  format: uri
                  example: https://woo.gnizdo.ddev.site
                consumer_key:
                  type: string
                  example: ck_...
                consumer_secret:
                  type: string
                  example: cs_...
                api_key:
                  type: string
                  example: etsy_key
                shop_id:
                  type: string
                  example: MyShop
                access_token:
                  type: string
                  example: token_xxx
                shop_url:
                  type: string
                  format: uri
                  example: https://my-shop.myshopify.com
            platform_profile:
              type: object
              nullable: true
              description: Platform-specific profile data (raw object, platform-neutral)
              additionalProperties: true

    ChannelSyncRunResource:
      type: object
      required:
        - id
        - channel_id
        - status
      properties:
        id:
          type: integer
        channel_id:
          type: integer
        status:
          type: string
          enum: [pending, running, success, failed]
        total_products:
          type: integer
        synced_products:
          type: integer
        failed_products:
          type: integer
        error_message:
          type: string
          nullable: true
        requires_reauth:
          type: boolean
          nullable: true
          description: Whether the canonical failed sync diagnostics require reauthorization before retrying
        is_stale:
          type: boolean
        stale_reason:
          type: string
          nullable: true
        stale_policy:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    ChannelStatusSyncRun:
      type: object
      required:
        - id
        - status
        - total_products
        - synced_products
        - failed_products
        - error_message
        - started_at
        - finished_at
        - created_at
        - updated_at
      properties:
        id:
          type: integer
        status:
          type: string
          enum: [pending, running, success, failed]
        total_products:
          type: integer
        synced_products:
          type: integer
        failed_products:
          type: integer
        error_message:
          type: string
          nullable: true
        requires_reauth:
          type: boolean
          nullable: true
          description: Whether the canonical failed sync diagnostics require reauthorization before retrying
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    ChannelState:
      type: string
      enum:
        - connected
        - sync_running
        - sync_failed
        - connection_lost
        - needs_attention

    ChannelConnectionStatus:
      type: object
      properties:
        verified:
          type: boolean
        checked_at:
          type: string
          format: date-time
          nullable: true
        latency_ms:
          type: integer
          nullable: true
        reason:
          type: string
          nullable: true

    ChannelStats:
      type: object
      properties:
        products:
          type: integer
          default: 0
        variations:
          type: integer
          default: 0
        in_stock:
          type: integer
          default: 0
        out_of_stock:
          type: integer
          default: 0

    ChannelSyncPayload:
      type: object
      properties:
        in_progress:
          type: boolean
        run_id:
          type: integer
          nullable: true
        status:
          type: string
          enum: [pending, running, success, failed]
          nullable: true

    ChannelSyncStepStatus:
      type: string
      enum:
        - pending
        - running
        - done
        - failed

    ChannelSyncStep:
      type: object
      required:
        - key
        - status
      properties:
        key:
          type: string
          enum: [connecting, fetching_products_variations, finalizing]
        status:
          $ref: "#/components/schemas/ChannelSyncStepStatus"
        title:
          type: string
          nullable: true
        subtitle:
          type: string
          nullable: true
        meta:
          type: object
          additionalProperties: true
          nullable: true
          description: |
            Optional progress metadata. For fetching steps, may include `counters`:
            - products
            - variations
            - total_products
            - categories
            - attributes

    ChannelSyncError:
      type: object
      properties:
        code:
          type: string
          nullable: true
        message:
          type: string
          nullable: true

    # Backward-compatible alias for older docs/tests
    ChannelStatusSyncError:
      type: object
      properties:
        code:
          type: string
          nullable: true
        message:
          type: string
          nullable: true

    ChannelSyncProgress:
      type: object
      required: [status, steps]
      properties:
        status:
          type: string
          enum: [running, success, failed]
        current_step:
          type: string
          enum: [connecting, fetching_products_variations, finalizing]
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        last_update_at:
          type: string
          format: date-time
          nullable: true
        steps:
          type: array
          items:
            $ref: "#/components/schemas/ChannelSyncStep"
        error:
          $ref: "#/components/schemas/ChannelSyncError"
          nullable: true

    ChannelStatusData:
      type: object
      required:
        - platform_code
        - state
        - connection
        - stats
        - sync
      properties:
        platform_code:
          type: string
          enum: [woo, etsy, shopify]
        state:
          $ref: "#/components/schemas/ChannelState"
        connection:
          $ref: "#/components/schemas/ChannelConnectionStatus"
        active:
          type: boolean
        last_sync:
          $ref: "#/components/schemas/ChannelStatusSyncRun"
          nullable: true
        last_successful_sync_at:
          type: string
          format: date-time
          nullable: true
        last_sync_attempt_at:
          type: string
          format: date-time
          nullable: true
        last_connection_check_at:
          type: string
          format: date-time
          nullable: true
        last_connection_check_status:
          type: string
          enum: [succeeded, failed]
          nullable: true
        last_sync_status:
          type: string
          enum: [success, partial, failed]
          nullable: true
        products_synced_count:
          type: integer
          nullable: true
        is_stale:
          type: boolean
        stats:
          $ref: "#/components/schemas/ChannelStats"
        sync:
          $ref: "#/components/schemas/ChannelSyncPayload"
        sync_progress:
          $ref: "#/components/schemas/ChannelSyncProgress"
          nullable: true

    ChannelStatusResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: "#/components/schemas/ChannelStatusData"

    ChannelSyncResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [sync_run]
          properties:
            sync_run:
              $ref: "#/components/schemas/ChannelSyncRunResource"

    SyncRunDetailResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [sync_run]
          properties:
            sync_run:
              $ref: "#/components/schemas/ChannelSyncRunResource"
            sync_progress:
              $ref: "#/components/schemas/ChannelSyncProgress"
              nullable: true

    WooStoreProfileResource:
      type: object
      required: [store_name, normalized, raw]
      properties:
        store_name:
          type: string
          nullable: true
        normalized:
          type: object
          properties:
            store_url:
              type: string
              nullable: true
            currency:
              type: string
              nullable: true
            country:
              type: string
              nullable: true
            region:
              type: string
              nullable: true
            timezone:
              type: string
              nullable: true
            locale:
              type: string
              nullable: true
            platform_version:
              type: string
              nullable: true
            price_decimals:
              type: integer
              nullable: true
            weight_unit:
              type: string
              nullable: true
            dimension_unit:
              type: string
              nullable: true
            tax_included:
              type: boolean
              nullable: true
        raw:
          type: object
          required:
            - wp_site
            - wc_settings
            - payment_gateways
            - shipping_methods
            - system_status
          properties:
            wp_site:
              type: object
              additionalProperties: true
            wc_settings:
              type: object
              additionalProperties: true
            payment_gateways:
              type: array
              items:
                type: object
                additionalProperties: true
            shipping_methods:
              type: array
              items:
                type: object
                additionalProperties: true
            system_status:
              type: object
              additionalProperties: true

    Channel:
      $ref: "#/components/schemas/ChannelResource"

    AdminUserListItem:
      type: object
      required:
        [id, email, created_at, status, activation_state, available_actions]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        created_at:
          type: string
          format: date-time
        signup_ip:
          type: string
          nullable: true
        referred_by:
          type: string
          format: email
          nullable: true
        invites:
          type: integer
        last_activity_at:
          type: string
          format: date-time
          nullable: true
        channels_count:
          type: integer
        stores_count:
          type: integer
        issues_7d:
          type: integer
        needs_attention:
          type: boolean
        latest_sync_at:
          type: string
          format: date-time
          nullable: true
        last_success_at:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum: [active, restricted, suspended]
        activation_state:
          type: string
          enum: [new, connected, synced]
          description: Lifecycle milestone state. `synced` means the user completed at least one successful sync in history and should not regress because of later failures.
        available_actions:
          $ref: "#/components/schemas/AdminUserAvailableActions"

    AdminUserDetail:
      allOf:
        - $ref: "#/components/schemas/AdminUserListItem"
        - type: object
          required:
            [updated_at, is_superuser, stores_count, store_connection_health]
          properties:
            updated_at:
              type: string
              format: date-time
            is_superuser:
              type: boolean
            stores_count:
              type: integer
            channels_count:
              type: integer
            issues_7d:
              type: integer
            needs_attention:
              type: boolean
            latest_sync_at:
              type: string
              format: date-time
              nullable: true
            last_success_at:
              type: string
              format: date-time
              nullable: true
            last_store_sync_at:
              type: string
              format: date-time
              nullable: true
            store_connection_health:
              type: string
              enum: [none, disconnected, attention, healthy]
            last_activity_at:
              type: string
              format: date-time
              nullable: true
            last_ip:
              type: string
              nullable: true
    AdminUserAvailableActions:
      type: object
      required: [can_impersonate, can_toggle_status]
      properties:
        can_impersonate:
          type: boolean
        can_toggle_status:
          type: boolean

    AdminChannel:
      type: object
      required:
        - id
        - platform_code
        - name
        - is_active
        - status
        - connection_status
      properties:
        id:
          type: integer
        platform:
          type: string
        platform_code:
          type: string
        name:
          type: string
        store_url:
          type: string
          nullable: true
        is_active:
          type: boolean
        status:
          type: string
        connection_status:
          type: string
        last_connection_at:
          type: string
          format: date-time
          nullable: true
        last_connection_error:
          type: string
          nullable: true
        last_synced_at:
          type: string
          format: date-time
          nullable: true
        last_sync_status:
          type: string
          nullable: true
        sync_status:
          type: string
          nullable: true
        last_sync_at:
          type: string
          format: date-time
          nullable: true
        last_error_reason:
          type: string
          nullable: true
        failure_class:
          type: string
          nullable: true
          enum: [transient, permanent]
        should_retry:
          type: boolean
          nullable: true
        retry_delay_seconds:
          type: integer
          nullable: true
        requires_reauth:
          type: boolean
          nullable: true
          description: Derived from the latest canonical failed sync diagnostics only
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    AdminStore:
      type: object
      required:
        - id
        - channel_id
        - channel_name
        - platform_code
        - connection_status
      properties:
        id:
          type: integer
        channel_id:
          type: integer
        channel_name:
          type: string
        platform:
          type: string
        platform_code:
          type: string
        store_url:
          type: string
          nullable: true
        store_name:
          type: string
          nullable: true
        store_status:
          type: string
          nullable: true
        store_checked_at:
          type: string
          format: date-time
          nullable: true
        connection_status:
          type: string
        store_connection_health:
          type: string
          enum: [disconnected, attention, healthy]
        last_synced_at:
          type: string
          format: date-time
          nullable: true
        last_successful_sync_at:
          type: string
          format: date-time
          nullable: true
        last_sync_status:
          type: string
          nullable: true
        sync_status:
          type: string
          nullable: true
        last_sync_at:
          type: string
          format: date-time
          nullable: true
        last_error_reason:
          type: string
          nullable: true
        failure_class:
          type: string
          nullable: true
          enum: [transient, permanent]
        should_retry:
          type: boolean
          nullable: true
        retry_delay_seconds:
          type: integer
          nullable: true
        requires_reauth:
          type: boolean
          nullable: true
          description: Derived from the latest canonical failed sync diagnostics only

    AdminUserListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [items]
          properties:
            items:
              type: array
              items:
                $ref: "#/components/schemas/AdminUserListItem"
        meta:
          type: object
          required: [total, page, per_page]
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer

    AdminUserDetailResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: "#/components/schemas/AdminUserDetail"

    AdminUserStatusUpdateRequest:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [active, restricted, suspended]
          description: Operational user status.

    AdminUserStatusUpdateResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: "#/components/schemas/UserResource"

    AdminWaitlistSignupAvailableActions:
      type: object
      required: [can_approve, can_approve_invite, can_reject, can_invite]
      properties:
        can_approve:
          type: boolean
        can_approve_invite:
          type: boolean
        can_reject:
          type: boolean
        can_invite:
          type: boolean

    AdminWaitlistSignup:
      type: object
      required:
        [
          id,
          email,
          platform,
          store_url,
          request_type,
          sku_count,
          recent_oversell_refund,
          current_sync_tool,
          status,
          ip,
          referrer,
          reviewed_at,
          invited_at,
          registered_user_id,
          created_at,
          available_actions,
        ]
      properties:
        id:
          type: integer
        email:
          type: string
          format: email
        platform:
          type: string
          nullable: true
        store_url:
          type: string
          nullable: true
        request_type:
          type: string
          enum: [beta_access, inventory_risk_review]
        sku_count:
          type: string
          nullable: true
        recent_oversell_refund:
          type: string
          nullable: true
        current_sync_tool:
          type: string
          nullable: true
        status:
          type: string
          enum: [pending, approved, invited, registered, rejected]
        ip:
          type: string
          nullable: true
        referrer:
          type: string
          nullable: true
        reviewed_at:
          type: string
          format: date-time
          nullable: true
        invited_at:
          type: string
          format: date-time
          nullable: true
        registered_user_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        available_actions:
          $ref: "#/components/schemas/AdminWaitlistSignupAvailableActions"

    AdminWaitlistSignupsResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [items]
          properties:
            items:
              type: array
              items:
                $ref: "#/components/schemas/AdminWaitlistSignup"
        meta:
          type: object
          required: [total, page, per_page]
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer

    AdminWaitlistSignupResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: "#/components/schemas/AdminWaitlistSignup"

    AdminUserImpersonationResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: "#/components/schemas/LoginResponse"
    ImpersonationExitResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [ended]
          properties:
            ended:
              type: boolean
              example: true

    AdminChannelsResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: "#/components/schemas/AdminChannel"

    AdminStoresResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: "#/components/schemas/AdminStore"

    ChannelDetails:
      $ref: "#/components/schemas/ChannelDetailsResource"

    ChannelSyncRun:
      $ref: "#/components/schemas/ChannelSyncRunResource"

    WooStoreProfile:
      $ref: "#/components/schemas/WooStoreProfileResource"

    FirstSyncResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [sync_run]
          properties:
            sync_run:
              $ref: "#/components/schemas/ChannelSyncRunResource"

    ChannelSyncRunListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: "#/components/schemas/ChannelSyncRunResource"
        meta:
          type: object
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer

    StoreProfileResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [platform, store_profile, last_sync_run]
          properties:
            platform:
              type: string
              example: woo
            store_profile:
              type: object
              nullable: true
              description: Platform-specific profile data (raw object)
              additionalProperties: true
            last_sync_run:
              oneOf:
                - $ref: "#/components/schemas/ChannelSyncRunResource"
                - type: "null"

    AfterSyncSummary:
      type: object
      required:
        - run_id
        - status
        - started_at
        - finished_at
        - total_products
        - synced_products
        - failed_products
      properties:
        run_id:
          type: integer
        status:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
        total_products:
          type: integer
          nullable: true
        synced_products:
          type: integer
          nullable: true
        failed_products:
          type: integer
          nullable: true

    AfterSyncData:
      type: object
      required:
        - sync_summary
        - store_summary
        - store_profile_is_stale
      properties:
        sync_summary:
          oneOf:
            - $ref: "#/components/schemas/AfterSyncSummary"
            - type: "null"
        store_summary:
          type: object
          nullable: true
          additionalProperties: true
          description: >
            Raw cached store-profile payload as returned by the current model
            `toArray()` shape. Field-level structure is not further normalized in
            current repository evidence.
        store_profile_is_stale:
          type: boolean

    AfterSyncResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: "#/components/schemas/AfterSyncData"

    CredentialField:
      type: object
      required:
        - name
        - label
        - type
      properties:
        name:
          type: string
          description: Credential field identifier.
        label:
          type: string
          description: Human-readable label rendered in the UI.
        type:
          type: string
          enum: [text, password]
          description: Input type for the field.
        placeholder:
          type: string
          nullable: true
          description: Optional placeholder text shown inside the input.
        help:
          type: string
          nullable: true
          description: Optional helper text shown below the input.

    PlatformDefinition:
      type: object
      required:
        - code
        - label
        - supported
        - credentials
      properties:
        code:
          type: string
          description: Platform identifier (matches `config/platforms.php`).
        label:
          type: string
          description: Display name for the platform dropdown.
        supported:
          type: boolean
          description: Whether the platform is live.
        credentials:
          type: array
          description: Credential fields for this platform.
          items:
            $ref: "#/components/schemas/CredentialField"

    HelpLimitsResponse:
      type: object
      required:
        - channel
        - channel_create
        - sync
        - account
      properties:
        channel:
          type: object
          required:
            - one_per_platform
          properties:
            one_per_platform:
              type: boolean
        channel_create:
          type: object
          required:
            - cooldown_seconds
            - warmup_max_per_day
          properties:
            cooldown_seconds:
              type: integer
              minimum: 0
            warmup_max_per_day:
              type: integer
              minimum: 0
        sync:
          type: object
          required:
            - cooldown_seconds
            - single_per_channel
          properties:
            cooldown_seconds:
              type: integer
              minimum: 0
            single_per_channel:
              type: boolean
        account:
          type: object
          required:
            - warmup_hours
          properties:
            warmup_hours:
              type: integer
              minimum: 0

    HelpLimitsRuntimeResponse:
      type: object
      required:
        - channel_create
        - sync
        - account
      properties:
        channel_create:
          type: object
          required:
            - blocked
            - retry_after
            - has_platform_conflict
          properties:
            blocked:
              type: boolean
            retry_after:
              type: integer
              minimum: 0
            has_platform_conflict:
              type: boolean
              description: Returns true only when the platform-specific conflict is knowable in the current request context; otherwise platform conflicts are checked at create time.
        sync:
          type: object
          required:
            - blocked
            - retry_after
          properties:
            blocked:
              type: boolean
            retry_after:
              type: integer
              minimum: 0
        account:
          type: object
          required:
            - in_warmup_window
            - warmup_hours
          properties:
            in_warmup_window:
              type: boolean
            warmup_hours:
              type: integer
              minimum: 0

    ChannelStoreRequest:
      type: object
      required:
        - platform_code
        - name
        - credentials
      properties:
        platform_code:
          type: string
          enum: [woo, etsy, shopify]
          example: woo
          description: Platform identifier (currently only WooCommerce is supported).
        name:
          type: string
          minLength: 1
          maxLength: 255
          example: My Channel
          description: Non-empty channel display name
        credentials:
          type: object
          example:
            store_url: https://woo.gnizdo.ddev.site
            consumer_key: ck_...
            consumer_secret: cs_...
          description: Platform API credentials (JSON object)
        status:
          type: string
          enum: [active, error, disabled]
          example: active
          description: Channel status
        connection_status:
          type: string
          enum: [disconnected, oauth_pending, connected, revoked, error]
          example: connected
          description: Canonical connection status for onboarding and sync flows
        is_active:
          type: boolean
          default: true
          example: true
          description: Whether the channel is active
        store_url:
          type: string
          nullable: true
          maxLength: 255
          description: Storefront URL from the platform metadata
          readOnly: true
        currency:
          type: string
          nullable: true
          maxLength: 8
          description: Store currency code
          readOnly: true
        country:
          type: string
          nullable: true
          maxLength: 32
          description: Store country code
          readOnly: true
        region:
          type: string
          nullable: true
          maxLength: 100
          description: Region/state for the store
          readOnly: true
        timezone:
          type: string
          nullable: true
          maxLength: 64
          description: Store timezone
          readOnly: true
        locale:
          type: string
          nullable: true
          maxLength: 10
          description: Store locale/language
          readOnly: true
        platform_version:
          type: string
          nullable: true
          maxLength: 50
          description: Platform version reported by the store
          readOnly: true
        price_decimals:
          type: integer
          nullable: true
          minimum: 0
          maximum: 8
          description: Number of decimals used for prices
          readOnly: true
        weight_unit:
          type: string
          nullable: true
          maxLength: 16
          description: Weight unit configured for the store
          readOnly: true
        dimension_unit:
          type: string
          nullable: true
          maxLength: 16
          description: Dimension unit configured for the store
          readOnly: true
        tax_included:
          type: boolean
          nullable: true
          description: Whether prices include tax
          readOnly: true
        connected_at:
          type: string
          format: date-time
          nullable: true
          description: Last successful connection timestamp
          readOnly: true
        last_connection_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T00:00:00.000000Z"
          description: Last connection timestamp
          readOnly: true
        last_connection_error:
          type: string
          nullable: true
          description: Safe connection error message (no secrets)
          readOnly: true
        last_synced_at:
          type: string
          format: date-time
          nullable: true
          description: Last successful catalog sync timestamp
          readOnly: true
        last_sync_status:
          type: string
          enum: [success, partial, failed]
          nullable: true
          description: Result of the last catalog sync
          readOnly: true
        last_sync_error:
          type: string
          nullable: true
          description: Safe sync error message (no secrets)
          readOnly: true

    UpdateChannelRequest:
      type: object
      description: All fields optional for partial updates
      properties:
        platform_code:
          type: string
          enum: [woo, etsy, shopify]
          example: woo
          description: Platform identifier (WooCommerce only; other platforms remain in beta).
        name:
          type: string
          maxLength: 255
          example: My Channel
          description: Channel display name
        credentials:
          type: object
          example:
            consumer_key: ck_...
            consumer_secret: cs_...
          description: Platform API credentials (JSON object)
        status:
          type: string
          enum: [active, error, disabled]
          example: active
          description: Channel status
        connection_status:
          type: string
          enum: [disconnected, oauth_pending, connected, revoked, error]
          example: connected
          description: Canonical connection status for onboarding and sync flows
        is_active:
          type: boolean
          example: true
          description: Whether the channel is active
        store_url:
          type: string
          nullable: true
          maxLength: 255
          description: Storefront URL from the platform metadata
          readOnly: true
        currency:
          type: string
          nullable: true
          maxLength: 8
          description: Store currency code
          readOnly: true
        country:
          type: string
          nullable: true
          maxLength: 32
          description: Store country code
          readOnly: true
        region:
          type: string
          nullable: true
          maxLength: 100
          description: Region/state for the store
          readOnly: true
        timezone:
          type: string
          nullable: true
          maxLength: 64
          description: Store timezone
          readOnly: true
        locale:
          type: string
          nullable: true
          maxLength: 10
          description: Store locale/language
          readOnly: true
        platform_version:
          type: string
          nullable: true
          maxLength: 50
          description: Platform version reported by the store
          readOnly: true
        price_decimals:
          type: integer
          nullable: true
          minimum: 0
          maximum: 8
          description: Number of decimals used for prices
          readOnly: true
        weight_unit:
          type: string
          nullable: true
          maxLength: 16
          description: Weight unit configured for the store
          readOnly: true
        dimension_unit:
          type: string
          nullable: true
          maxLength: 16
          description: Dimension unit configured for the store
          readOnly: true
        tax_included:
          type: boolean
          nullable: true
          description: Whether prices include tax
          readOnly: true
        connected_at:
          type: string
          format: date-time
          nullable: true
          description: Last successful connection timestamp
          readOnly: true
        last_connection_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T00:00:00.000000Z"
          description: Last connection timestamp
          readOnly: true
        last_connection_error:
          type: string
          nullable: true
          description: Safe connection error message (no secrets)
          readOnly: true
        last_synced_at:
          type: string
          format: date-time
          nullable: true
          description: Last successful catalog sync timestamp
          readOnly: true
        last_sync_status:
          type: string
          enum: [success, partial, failed]
          nullable: true
          description: Result of the last catalog sync
          readOnly: true
        last_sync_error:
          type: string
          nullable: true
          description: Safe sync error message (no secrets)
          readOnly: true

    WooProductChildResource:
      type: object
      required:
        - id
        - parent_id
        - sku
        - name
        - type
        - regular_price
        - sale_price
        - stock_quantity
        - stock_status
        - gtin
        - brand
        - categories
        - tags
        - sync_status
        - last_sync_error
        - signals
        - platform_updated_at
      properties:
        id:
          type: integer
          example: 2
          description: Catalog product ID
        parent_id:
          type: integer
          example: 1
          description: Parent product ID
        sku:
          type: string
          nullable: true
          example: "VAR-001"
          description: Variation SKU
        name:
          type: string
          nullable: true
          example: "Product Name · Red"
          description: Product name
        type:
          type: string
          enum: [simple, variable, variation, grouped]
          example: variation
          description: Product type
        regular_price:
          type: string
          nullable: true
          example: "99.9900"
          description: Regular price (decimal)
        sale_price:
          type: string
          nullable: true
          example: "89.9900"
          description: Sale price (decimal)
        stock_quantity:
          type: integer
          nullable: true
          example: 5
          description: Current quantity
        stock_status:
          type: string
          enum: [in_stock, out_of_stock, on_backorder]
          example: in_stock
          description: Stock availability
        gtin:
          type: string
          nullable: true
          example: "00012345678905"
          description: Global Trade Item Number (GTIN) from Woo metadata
        brand:
          type: string
          nullable: true
          example: "Test Brand"
          description: Product brand metadata
        categories:
          type: array
          items:
            type: string
          nullable: true
          description: Catalog category slugs/names
        tags:
          type: array
          items:
            type: string
          nullable: true
          description: Tag slugs/names for the product
        sync_status:
          type: string
          enum: [synced, pending, failed]
          example: synced
          description: Per-product sync status
        last_sync_error:
          type: string
          nullable: true
          description: Safe sync error description for this product
        signals:
          $ref: "#/components/schemas/CatalogProductSignals"
        platform_updated_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T11:05:00Z"
          description: Platform last update timestamp

    WooProductResource:
      type: object
      required:
        - id
        - parent_id
        - sku
        - name
        - type
        - regular_price
        - sale_price
        - stock_quantity
        - stock_status
        - gtin
        - brand
        - categories
        - tags
        - sync_status
        - last_sync_error
        - signals
        - platform_updated_at
      properties:
        id:
          type: integer
          example: 1
          description: Catalog product ID
        parent_id:
          type: integer
          nullable: true
          example: null
          description: Parent product ID (null for root rows)
        sku:
          type: string
          nullable: true
          example: "PROD-001"
          description: Product SKU
        name:
          type: string
          nullable: true
          example: "Product Name"
          description: Product name
        type:
          type: string
          enum: [simple, variable, variation, grouped]
          example: variable
          description: Product type
        regular_price:
          type: string
          nullable: true
          example: "99.9900"
          description: Regular price (decimal)
        sale_price:
          type: string
          nullable: true
          example: null
          description: Sale price (decimal)
        stock_quantity:
          type: integer
          nullable: true
          example: 10
          description: Current quantity
        stock_status:
          type: string
          enum: [in_stock, out_of_stock, on_backorder]
          example: in_stock
          description: Stock availability
        gtin:
          type: string
          nullable: true
          example: "00012345678905"
          description: Global Trade Item Number (GTIN) from Woo metadata
        brand:
          type: string
          nullable: true
          example: "Test Brand"
          description: Product brand metadata
        categories:
          type: array
          items:
            type: string
          nullable: true
          description: Catalog category slugs/names
        tags:
          type: array
          items:
            type: string
          nullable: true
          description: Tag slugs/names for the product
        sync_status:
          type: string
          enum: [synced, pending, failed]
          example: synced
          description: Per-product sync status
        last_sync_error:
          type: string
          nullable: true
          description: Safe sync error description for this product
        signals:
          $ref: "#/components/schemas/CatalogProductSignals"
        platform_updated_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-01-01T11:00:00Z"
          description: Platform last update timestamp
        children:
          type: array
          description: Variation records that belong to this parent
          items:
            $ref: "#/components/schemas/WooProductChildResource"

    CatalogProductSignals:
      type: object
      required: [price, stock]
      properties:
        price:
          $ref: "#/components/schemas/CatalogPriceSignal"
        stock:
          $ref: "#/components/schemas/CatalogStockSignal"

    CatalogPriceSignal:
      type: object
      required: [has_price, status]
      properties:
        has_price:
          type: boolean
          description: Whether the row has a price signal.
        status:
          type: string
          enum: [ok, missing, from_variants]
          description: Price signal status for the row.

    CatalogStockSignal:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [tracked, not_tracked, unknown]
          description: Stock signal status for the row.
        qty:
          type: integer
          nullable: true
          description: Quantity available when stock is tracked.

    StockStatus:
      type: string
      enum: [in_stock, out_of_stock, on_backorder]
      description: Current stock availability states propagated to the platform.

    CatalogProductItemRequest:
      type: object
      minProperties: 1
      properties:
        regular_price:
          oneOf:
            - type: number
              minimum: 0
            - type: string
              pattern: '^\d+(\.\d+)?$'
          nullable: true
          example: "49.9900"
          description: Regular price accepted by Laravel numeric validation as a number or numeric string.
        sale_price:
          oneOf:
            - type: number
              minimum: 0
            - type: string
              pattern: '^\d+(\.\d+)?$'
          nullable: true
          example: "39.9900"
          description: Sale price accepted by Laravel numeric validation as a number or numeric string.
        stock_quantity:
          type: integer
          nullable: true
          minimum: 0
          example: 10
          description: Quantity available for the item
        stock_status:
          $ref: "#/components/schemas/StockStatus"

    CatalogSyncResponse:
      type: object
      properties:
        synced:
          type: integer
          example: 134
          description: Number of records that were upserted during sync
        failed:
          type: integer
          example: 5
          description: Number of records that failed to sync
        errors:
          type: array
          items:
            type: string
          description: Safe error descriptions (if any)

    OnboardingStatusResponse:
      type: object
      required:
        [
          effectiveState,
          connectedChannelsCount,
          connections,
          canRunFirstSync,
          nextRecommendedAction,
          onboarding,
        ]
      properties:
        effectiveState:
          type: string
          enum:
            [
              new,
              channels_connecting,
              first_sync_pending,
              first_sync_running,
              ready,
              blocked,
            ]
        connectedChannelsCount:
          type: integer
        connections:
          type: array
          items:
            $ref: "#/components/schemas/ChannelConnection"
        activeRun:
          type: object
          nullable: true
          description: Present only when a non-stale onboarding first-sync run still blocks readiness after canonical trusted-channel reconciliation.
          properties:
            id: { type: string, format: uuid }
            status: { type: string }
            baselineDate: { type: string, nullable: true }
            startedAt: { type: string, format: date-time, nullable: true }
            finishedAt: { type: string, format: date-time, nullable: true }
        lastFinishedRun:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            status: { type: string }
            baselineDate: { type: string, nullable: true }
            usableData: { type: boolean, nullable: true }
            dailyRunId: { type: string, format: uuid, nullable: true }
        canRunFirstSync:
          type: boolean
        nextRecommendedAction:
          type: string
          enum:
            [
              connect_channel,
              wait_first_sync,
              run_first_sync,
              go_to_daily_check,
            ]
        blockedMeta:
          allOf:
            - $ref: "#/components/schemas/BlockedMeta"
          nullable: true
        onboarding:
          $ref: "#/components/schemas/OnboardingAuthShellState"

    ChannelConnection:
      type: object
      required: [channel, status]
      properties:
        channelId:
          type: integer
          nullable: true
        channel:
          type: string
          enum: [woocommerce, shopify, etsy, ebay]
        status:
          type: string
          enum: [disconnected, oauth_pending, connected, revoked, error]
        capabilities:
          type: object
          nullable: true
          properties:
            read:
              type: boolean
            write:
              type: boolean
          required: [read, write]
        connectedAt:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: object
          nullable: true
          additionalProperties: true
        lastError:
          type: string
          nullable: true

    OnboardingAuthShellState:
      type: object
      required: [required, step, provider]
      properties:
        required:
          type: boolean
        step:
          type: string
          enum: [choose_platform, access_details, connecting, done]
        provider:
          type: string
          nullable: true
          enum: [woo, shopify, etsy, ebay]
        blockedReason:
          type: string
          nullable: true
        lastError:
          type: string
          nullable: true

    ChannelIssueDiagnostics:
      type: object
      required:
        - user_id
        - channel_id
        - current_backend_health_state
        - last_successful_sync_at
        - last_sync_attempt_at
        - last_sync_status
        - last_connection_check_at
        - last_connection_check_status
        - active_sync_run_id
      properties:
        user_id:
          type: integer
          nullable: true
        channel_id:
          type: integer
        current_backend_health_state:
          type: string
          nullable: true
        last_successful_sync_at:
          type: string
          format: date-time
          nullable: true
        last_sync_attempt_at:
          type: string
          format: date-time
          nullable: true
        last_sync_status:
          type: string
          nullable: true
        last_connection_check_at:
          type: string
          format: date-time
          nullable: true
        last_connection_check_status:
          type: string
          nullable: true
        active_sync_run_id:
          type: integer
          nullable: true

    ChannelIssueReportRequest:
      type: object
      required: [diagnostics]
      properties:
        message:
          type: string
          nullable: true
          maxLength: 1000
        diagnostics:
          $ref: "#/components/schemas/ChannelIssueDiagnostics"

    ChannelIssueReportResponse:
      type: object
      required: [success, message]
      properties:
        success:
          type: boolean
        message:
          type: string
        log_ref:
          type: string
          nullable: true

    BlockedMeta:
      type: object
      required: [primaryReason, reasons]
      properties:
        primaryReason:
          type: string
          enum:
            - ALL_TARGETED_CHANNELS_FAILED
            - NO_USABLE_DATA
            - STORE_EMPTY_OR_NO_MATCHES
            - PERMISSIONS_OR_SCOPE_ISSUE
            - AUTH_EXPIRED_OR_REVOKED
            - API_RATE_LIMIT_OR_OUTAGE
            - DATA_MISSING_CRITICAL
            - UNKNOWN
        reasons:
          type: array
          items:
            type: string
            enum:
              - ALL_TARGETED_CHANNELS_FAILED
              - NO_USABLE_DATA
              - STORE_EMPTY_OR_NO_MATCHES
              - PERMISSIONS_OR_SCOPE_ISSUE
              - AUTH_EXPIRED_OR_REVOKED
              - API_RATE_LIMIT_OR_OUTAGE
              - DATA_MISSING_CRITICAL
              - UNKNOWN
        debugCodes:
          type: array
          items:
            type: string
          nullable: true
        channelsFailed:
          type: array
          items:
            $ref: "#/components/schemas/ChannelFailure"
          nullable: true

    FirstSyncRunRequest:
      type: object
      properties:
        channels:
          type: array
          items:
            type: string
            enum: [woocommerce, shopify, etsy, ebay]

    FirstSyncRunResponse:
      type: object
      required: [runId, status]
      properties:
        runId:
          type: string
          format: uuid
        status:
          type: string
          enum: [queued, running, success, partial, failed]
        baselineDate:
          type: string
          nullable: true
        dailyRunId:
          type: string
          format: uuid
          nullable: true

    FirstSyncRunStatusResponse:
      type: object
      required: [id, status]
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [queued, running, success, partial, failed]
        startedAt:
          type: string
          format: date-time
          nullable: true
        finishedAt:
          type: string
          format: date-time
          nullable: true
        channelsTargeted:
          type: array
          items:
            type: string
            enum: [woocommerce, shopify, etsy, ebay]
        channelsSucceeded:
          type: array
          items:
            type: string
            enum: [woocommerce, shopify, etsy, ebay]
        channelsFailed:
          type: array
          items:
            $ref: "#/components/schemas/ChannelFailure"
        usableData:
          type: boolean
          nullable: true
        baselineDate:
          type: string
          nullable: true
        dailyRunId:
          type: string
          format: uuid
          nullable: true
        reportAvailable:
          type: boolean

    DailyReportJson:
      type: object
      required:
        - reportMeta
        - systemPulse
        - criticalFirst
        - driftSignals
        - changeLog
        - freshness
        - channelHealth
        - patterns
        - actionList
        - stabilityTrend
      properties:
        reportMeta:
          type: object
          required:
            [isBaseline, date, timezone, generatedAt, dailyRunId, version]
          properties:
            isBaseline: { type: boolean }
            date: { type: string }
            timezone: { type: string }
            generatedAt: { type: string, format: date-time }
            dailyRunId: { type: string, format: uuid }
            version: { type: string, enum: ["1.0"] }
        systemPulse:
          type: object
          required:
            [
              systemState,
              itemsToReviewCount,
              criticalRisksCount,
              channelsHealthyCount,
              channelsTotal,
            ]
          properties:
            systemState: { type: string, enum: [ok, drift_forming, critical] }
            itemsToReviewCount: { type: integer }
            criticalRisksCount: { type: integer }
            channelsHealthyCount: { type: integer }
            channelsTotal: { type: integer }
        criticalFirst:
          type: array
          items:
            type: object
            required:
              [
                findingId,
                gnizdoItemId,
                severity,
                type,
                title,
                whatChanged,
                impactToday,
                observedAt,
                observedAgeMinutes,
                channels,
                links,
              ]
            properties:
              findingId: { type: string }
              gnizdoItemId: { type: string }
              severity: { type: string, enum: [critical, high, medium] }
              type:
                type: string
                enum:
                  [
                    stock_drift,
                    price_drift,
                    channel_lag,
                    data_missing,
                    currency_mismatch,
                  ]
              title: { type: string }
              whatChanged: { type: string }
              impactToday:
                type: string
                enum: [refund, margin_risk, unknown]
              observedAt: { type: string, format: date-time }
              observedAgeMinutes: { type: integer }
              channels:
                type: array
                items:
                  { type: string, enum: [woocommerce, shopify, etsy, ebay] }
              links:
                type: array
                items:
                  type: object
                  required: [channel, url]
                  properties:
                    channel:
                      { type: string, enum: [woocommerce, shopify, etsy, ebay] }
                    url: { type: string }
        driftSignals:
          type: object
          required:
            [
              inventoryDriftCount,
              priceDriftCount,
              channelsLagging,
              dataMissingCount,
            ]
          properties:
            inventoryDriftCount: { type: integer }
            priceDriftCount: { type: integer }
            channelsLagging:
              type: array
              items:
                type: object
                required: [channel, reason]
                properties:
                  channel:
                    { type: string, enum: [woocommerce, shopify, etsy, ebay] }
                  reason: { type: string }
                  lagMinutes: { type: integer, nullable: true }
            dataMissingCount: { type: integer }
            notes:
              type: array
              items: { type: string }
        changeLog:
          type: object
          required:
            [
              mode,
              baseline,
              stockChangedCount,
              priceChangedCount,
              itemsAppearedCount,
              itemsDisappearedCount,
            ]
          properties:
            mode: { type: string, enum: [snapshot_diff] }
            baseline: { type: boolean }
            stockChangedCount: { type: integer }
            priceChangedCount: { type: integer }
            itemsAppearedCount: { type: integer }
            itemsDisappearedCount: { type: integer }
            channelFreshnessChanged:
              type: array
              items: {}
        freshness:
          type: object
          required:
            [
              timestamp,
              source,
              confidence,
              delay,
            ]
          properties:
            timestamp:
              type: string
              format: date-time
              description: Timestamp when GNIZDO evaluated the report freshness summary.
            source:
              type: string
              enum: [channel_states]
              description: Current source used to derive the freshness summary.
            confidence:
              type: string
              enum: [high, medium, low]
              description: Confidence in the freshness summary based on available channel state evidence.
            delay:
              type: object
              required: [seconds, reason]
              properties:
                seconds:
                  type: integer
                  nullable: true
                  description: Delay beyond the freshness threshold when GNIZDO can compute it.
                reason:
                  type: string
                  enum:
                    [
                      none,
                      freshness_window_exceeded,
                      missing_last_successful_sync_at,
                      no_channel_states,
                      stale_runtime_state,
                    ]
                  description: Why the current freshness delay is zero, known, unknown, or only legacy-normalized.
        channelHealth:
          type: array
          items:
            type: object
            required:
              [
                channel,
                status,
                timestamp,
                source,
                confidence,
                delay,
              ]
            properties:
              channel:
                { type: string, enum: [woocommerce, shopify, etsy, ebay] }
              status:
                type: string
                enum: [healthy, lagging, error, unknown]
                description: Canonical channel trust/health status derived from current channel state evidence.
              timestamp:
                type: string
                format: date-time
                description: Timestamp when GNIZDO evaluated the channel health projection.
              source:
                type: string
                enum: [channel_states]
                description: Current source used to derive the channel health projection.
              confidence:
                type: string
                enum: [high, medium, low]
                description: Confidence in the channel health projection based on available channel state evidence.
              delay:
                type: object
                required: [seconds, reason]
                properties:
                  seconds:
                    type: integer
                    nullable: true
                    description: Delay derived from current lag evidence when GNIZDO can compute it.
                  reason:
                    type: string
                    enum:
                      [
                        none,
                        lag_detected,
                        lag_unknown,
                        api_error,
                        unknown,
                      ]
                    description: Why the current channel trust state is delayed, degraded, or fully healthy.
        patterns:
          type: object
          required: [enabled, items]
          properties:
            enabled: { type: boolean }
            items:
              type: array
              items:
                type: object
                required: [hypothesis]
                properties:
                  hypothesis: { type: string }
                  confidence:
                    { type: string, enum: [low, med, high], nullable: true }
                  relatedChannels:
                    type: array
                    items:
                      { type: string, enum: [woocommerce, shopify, etsy, ebay] }
        actionList:
          type: array
          items:
            type: object
            required:
              [
                findingId,
                gnizdoItemId,
                severity,
                type,
                title,
                whatChanged,
                impactToday,
                observedAt,
                observedAgeMinutes,
                channels,
                links,
              ]
            properties:
              findingId: { type: string }
              gnizdoItemId: { type: string }
              severity: { type: string, enum: [critical, high, medium] }
              type:
                type: string
                enum:
                  [
                    stock_drift,
                    price_drift,
                    channel_lag,
                    data_missing,
                    currency_mismatch,
                  ]
              title: { type: string }
              whatChanged: { type: string }
              impactToday:
                type: string
                enum: [refund, margin_risk, unknown]
              observedAt: { type: string, format: date-time }
              observedAgeMinutes: { type: integer }
              channels:
                type: array
                items:
                  { type: string, enum: [woocommerce, shopify, etsy, ebay] }
              links:
                type: array
                items:
                  type: object
                  required: [channel, url]
                  properties:
                    channel:
                      { type: string, enum: [woocommerce, shopify, etsy, ebay] }
                    url: { type: string }
        stabilityTrend:
          type: object
          required:
            [
              windowDays,
              driftTrend,
              conflictsCount,
              priceMismatchesCount,
              inventoryDriftsCount,
            ]
          properties:
            windowDays: { type: integer, enum: [7] }
            driftTrend:
              { type: string, enum: [increasing, stable, improving, unknown] }
            conflictsCount: { type: integer }
            priceMismatchesCount: { type: integer }
            inventoryDriftsCount: { type: integer }
    DailyEvent:
      type: object
      required:
        - id
        - daily_run_id
        - channel
        - event_type
        - entity_type
        - entity_id
        - prev_snapshot_id
        - next_snapshot_id
        - field
        - severity
        - detected_at
      properties:
        id:
          type: string
          format: uuid
        daily_run_id:
          type: string
          format: uuid
        channel:
          type: string
          enum: [woocommerce, shopify, etsy, ebay]
        event_type:
          type: string
          enum: [PRICE_CHANGE, STOCK_CHANGE, LISTING_STATUS_CHANGE]
        entity_type:
          type: string
        entity_id:
          type: string
        prev_snapshot_id:
          type: string
          format: uuid
        next_snapshot_id:
          type: string
          format: uuid
        field:
          type: string
          enum: [price, stock, listing_status]
        previous_value:
          nullable: true
          oneOf:
            - type: string
            - type: number
            - type: boolean
            - type: object
            - type: array
        new_value:
          nullable: true
          oneOf:
            - type: string
            - type: number
            - type: boolean
            - type: object
            - type: array
        severity:
          type: string
          enum: [LOW, MEDIUM, HIGH]
        detected_at:
          type: string
          format: date-time

    ChannelFailure:
      type: object
      additionalProperties: true
      properties:
        channel:
          type: string
          enum: [woocommerce, shopify, etsy, ebay]
        code:
          type: string
          enum:
            - AUTH_REVOKED
            - SCOPE_MISSING
            - HTTP_429
            - HTTP_5XX
            - TIMEOUT
            - NETWORK
            - DATA_MISSING_CRITICAL
            - UNKNOWN
        retryable:
          type: boolean
        message:
          type: string
          nullable: true

    ChannelConnectRequest:
      type: object
      properties:
        storeUrl:
          type: string
        consumerKey:
          type: string
        consumerSecret:
          type: string

    ChannelConnectResponse:
      type: object
      properties:
        connected:
          type: boolean
          example: true
        redirectUrl:
          type: string
          nullable: true

tags:
  - name: Authentication
    description: User authentication endpoints
  - name: Platforms
    description: Platform definitions and metadata used to render credential forms
  - name: Channels
    description: Platform channel management (new term)
  - name: Onboarding
    description: Onboarding state and first sync endpoints
  - name: Catalog
    description: Catalog retrieval and synchronization
  - name: System
    description: System health and version endpoints
  - name: Documentation
    description: API documentation endpoints
