openapi: 3.1.0
info:
  title: ValidonX API
  version: "1.0.0"
  description: |
    Multi-tenant SaaS licensing, activation, and entitlement platform.

    ## Authentication

    **Integration API** (`/v1/integration/*`): Use `X-API-Key` header with your secret API key.

    **Admin API** (`/api/auth/*`, `/v1/admin/*`): Use `Authorization: Bearer <token>` from login.

    **Tenant/Onboarding API** (`/v1/auth/*`, `/v1/onboarding/*`, `/v1/account/*`): Use `Authorization: Bearer <token>` from registration or login.

    ## Response Format

    All responses follow the envelope format:
    ```json
    {
      "data": { ... },
      "meta": { "request_id": "uuid", "api_version": "1" }
    }
    ```

    Errors:
    ```json
    {
      "error": { "code": "DOMAIN.CATEGORY", "message": "...", "type": "...", "status": 429 },
      "meta": { "request_id": "uuid", "api_version": "1" }
    }
    ```

    ## Rate Limiting

    All Integration API responses include:
    - `X-RateLimit-Limit` — requests allowed per window
    - `X-RateLimit-Remaining` — requests remaining
    - `X-RateLimit-Reset` — Unix timestamp when window resets
    - `Retry-After` — seconds to wait (on 429 responses)

    ## Coverage

    This spec documents the **externally-facing surface** that integrators
    and tenant admins rely on:

    - **Integration API** (`/v1/integration/*`) — runtime license validation,
      activation, entitlements, and usage recording. Called from the
      integrator's own software using their tenant's API key.
    - **Tenant Management API** (`/v1/tenant/*`) — tenant admin dashboard
      operations: license CRUD, API key management, webhook configuration,
      audit logs, settings. Called from the integrator's admin UI using a
      user Bearer token.
    - **Customer Billing** (`/v1/billing/*`) — the integrator's own
      subscription, invoices, payment methods, and notification preferences.
    - **Onboarding, user auth (incl. 2FA), account self-service, marketing,
      consent, and monitoring** — public and user-authenticated endpoints
      for the sign-up and account-management flows.

    **Not documented here** (internal-only, subject to change):
    - Admin (`/v1/admin/*`) — platform staff tools for tenant lifecycle,
      billing ops, metrics, and user management. Includes the admin
      tenant-scoped route group (`/v1/admin/tenants/{tenantId}/*`)
      which mirrors the public tenant API for platform staff acting
      on behalf of a tenant; same operation semantics as `/v1/tenant/*`,
      additional `AuthenticateAdmin + ResolveTenantFromPath` middleware
      stack, and admin-initiated audit events record
      `actor_type='admin'` with a `_BY_ADMIN` suffix on the event code.
    - Incoming webhooks (`/webhooks/stripe`, `/webhooks/vectis/*`) — inbound
      delivery endpoints consumed by ValidonX itself
  contact:
    email: support@validonx.com
    url: https://validonx.com
  license:
    name: Proprietary

servers:
  - url: https://api.validonx.com/api
    description: Production
  - url: http://localhost:8000/api
    description: Local development

tags:
  - name: Integration
    description: License validation, activation, entitlements, and usage recording (X-API-Key auth)
  - name: Auth
    description: Admin login, logout, and profile
  - name: Onboarding
    description: Self-service registration, email verification, plan selection, checkout, provisioning
  - name: Account
    description: Authenticated user self-service — profile, password, email change, 2FA, GDPR data export/deletion
  - name: User Auth
    description: Dashboard user login, logout, profile, and 2FA challenge (separate from admin auth)
  - name: Admin
    description: Platform administration (tenant management, metrics, audit, rate limits)
  - name: Tenant
    description: Tenant management API — license/API key/webhook CRUD, settings, audit logs, notifications (Bearer token with `tenant` ability)
  - name: Billing
    description: Customer billing portal (invoices, subscription, payment methods, notification preferences)
  - name: Marketing
    description: Newsletter subscription
  - name: Monitoring
    description: Health checks and Prometheus metrics

paths:
  # ── Integration API ──────────────────────────────────────────────
  /v1/integration/licenses:
    post:
      tags: [Integration]
      summary: Issue a license key
      description: >
        Mint a license for a customer who purchased one of your own products.
        Send the authoritative `entitlements` for the purchased tier plus an
        `activation_limit` (use `0` for unlimited). The `product_code` must
        already be registered in your ValidonX catalog — issuance never
        auto-creates a product, so register a product before selling it.


        Idempotent on `X-Idempotency-Key` (one key per purchased line-item,
        forever): a repeat call with the same key returns the same license
        instead of minting a duplicate (`meta.idempotent_replay: true`).
      operationId: issueLicense
      security:
        - ApiKeyAuth: []
      parameters:
        - name: X-Idempotency-Key
          in: header
          required: false
          description: Per-line-item idempotency key (e.g. `ord-100000231-item-5512`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [product_code]
              properties:
                product_code:
                  type: string
                  description: Code of a product already registered in your catalog (e.g. `cps.smtp`).
                entitlements:
                  type: object
                  description: Your authoritative feature flags for the purchased tier (stored verbatim).
                plan:
                  type: string
                  description: Plan label for reporting (e.g. `free`, `pro`, `enterprise`).
                activation_limit:
                  type: integer
                  minimum: 0
                  description: Max activations for this license. `0` = unlimited.
                type:
                  type: string
                  enum: [perpetual, subscription, trial]
                  default: perpetual
                expires_at:
                  type: string
                  format: date-time
                  description: >-
                    License expiry (RFC 3339 date-time). Omit this field entirely for a
                    perpetual license — do NOT send `null`, which the API rejects with a
                    422. Provide a date-time only for a `subscription` or `trial` license.
                idempotency_key:
                  type: string
                  description: Fallback transport for the idempotency key when the header cannot be set.
                customer:
                  type: object
                  description: Optional buyer details, stored verbatim on the license for support traceability.
                  properties:
                    email:
                      type: string
                      format: email
                    name:
                      type: string
                reference:
                  type: object
                  description: Optional order attribution (e.g. order_increment_id, order_item_id, sku, source), stored verbatim.
      responses:
        "201":
          description: License issued (or idempotently replayed)
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: The product_code is not registered in your catalog (CATALOG.INVALID_PRODUCT).
        "422":
          description: Validation error
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/integration/licenses/{licenseKey}/validate:
    post:
      tags: [Integration]
      summary: Validate a license key
      operationId: validateLicense
      security:
        - ApiKeyAuth: []
      parameters:
        - name: licenseKey
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: License validation result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseValidationResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/integration/activations:
    post:
      tags: [Integration]
      summary: Create an activation
      operationId: createActivation
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [license_key, fingerprint]
              properties:
                license_key:
                  type: string
                fingerprint:
                  type: string
                metadata:
                  type: object
      responses:
        "201":
          description: Activation created
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/integration/activations/{activationId}/validate:
    post:
      tags: [Integration]
      summary: Validate an activation
      operationId: validateActivation
      security:
        - ApiKeyAuth: []
      parameters:
        - name: activationId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Activation validation result
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/integration/entitlements/check:
    post:
      tags: [Integration]
      summary: Check entitlements
      operationId: checkEntitlements
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                entitlement_codes:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Entitlement check results

  /v1/integration/usage/record:
    post:
      tags: [Integration]
      summary: Record a usage event
      operationId: recordUsage
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [metric, value]
              properties:
                metric:
                  type: string
                value:
                  type: number
                metadata:
                  type: object
      responses:
        "201":
          description: Usage event recorded

  # ── Auth ──────────────────────────────────────────────────────────
  /auth/login:
    post:
      tags: [Auth]
      summary: Admin login
      operationId: adminLogin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        "200":
          description: Login successful, returns token
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      token:
                        type: string
                      admin:
                        $ref: "#/components/schemas/AdminProfile"
        "401":
          description: Invalid credentials
        "429":
          description: Account locked (brute-force protection)

  /auth/logout:
    post:
      tags: [Auth]
      summary: Admin logout
      operationId: adminLogout
      security:
        - BearerAuth: []
      responses:
        "204":
          description: Logged out

  /auth/me:
    get:
      tags: [Auth]
      summary: Get admin profile
      operationId: adminProfile
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Admin profile

  # ── Onboarding ───────────────────────────────────────────────────
  /v1/auth/register:
    post:
      tags: [Onboarding]
      summary: Register a new user account
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, password, password_confirmation, company]
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
                password_confirmation:
                  type: string
                company:
                  type: string
      responses:
        "201":
          description: Account created, returns token
        "422":
          description: Validation error

  /v1/auth/forgot-password:
    post:
      tags: [Onboarding]
      summary: Request password reset link
      operationId: forgotPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Always returns success (anti-enumeration)

  /v1/auth/reset-password:
    post:
      tags: [Onboarding]
      summary: Reset password with token
      operationId: resetPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, email, password, password_confirmation]
              properties:
                token:
                  type: string
                email:
                  type: string
                  format: email
                password:
                  type: string
                password_confirmation:
                  type: string
      responses:
        "200":
          description: Password reset successfully
        "422":
          description: Invalid or expired token

  /v1/auth/email/verify:
    post:
      tags: [Onboarding]
      summary: Verify email address
      operationId: verifyEmail
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Email verified

  /v1/auth/email/resend:
    post:
      tags: [Onboarding]
      summary: Resend verification email
      operationId: resendVerification
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Verification email sent

  # ── User Auth (Dashboard) ────────────────────────────────────────
  /v1/auth/login:
    post:
      tags: [User Auth]
      summary: Dashboard user login
      operationId: userLogin
      description: |
        Returns a bearer token on success. If the user has 2FA enabled, responds
        with `403 AUTH.TWO_FACTOR_REQUIRED` and `error.details.challenge_token` —
        the caller must POST that token plus a TOTP or recovery code to
        `/v1/auth/login/challenge`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        "200":
          description: Authenticated; returns bearer token
        "401":
          description: Invalid credentials
        "403":
          description: Two-factor required (see `error.details.challenge_token`)

  /v1/auth/login/challenge:
    post:
      tags: [User Auth]
      summary: Complete 2FA login challenge
      operationId: userLoginChallenge
      description: |
        Exchange a single-use challenge token plus a TOTP code (6 digits) or a
        recovery code for a bearer token. Challenge tokens expire after 5 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [challenge_token]
              properties:
                challenge_token:
                  type: string
                totp_code:
                  type: string
                  description: 6-digit TOTP code from the authenticator app
                recovery_code:
                  type: string
                  description: Single-use recovery code (alternative to totp_code)
      responses:
        "200":
          description: Authenticated; returns bearer token
        "422":
          description: Invalid or expired challenge / code

  /v1/auth/logout:
    post:
      tags: [User Auth]
      summary: Revoke the current user bearer token
      operationId: userLogout
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Token revoked

  /v1/auth/me:
    get:
      tags: [User Auth]
      summary: Get the authenticated user profile
      operationId: userMe
      security:
        - BearerAuth: []
      responses:
        "200":
          description: User profile + tenant context

  /v1/plans:
    get:
      tags: [Onboarding]
      summary: List available subscription plans
      operationId: listPlans
      responses:
        "200":
          description: Active plans with entitlements

  /v1/onboarding/checkout-session:
    post:
      tags: [Onboarding]
      summary: Create Stripe Checkout session for paid plan
      operationId: createCheckoutSession
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan_id, success_url, cancel_url]
              properties:
                plan_id:
                  type: string
                  format: uuid
                success_url:
                  type: string
                  format: uri
                cancel_url:
                  type: string
                  format: uri
      responses:
        "200":
          description: Checkout URL or free plan confirmation

  /v1/onboarding/provision:
    post:
      tags: [Onboarding]
      summary: Provision a new tenant workspace
      operationId: provisionTenant
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company, plan_code]
              properties:
                company:
                  type: string
                plan_code:
                  type: string
      responses:
        "201":
          description: Tenant provisioned, returns API key (shown once)

  # ── Account (Profile & Security) ────────────────────────────────
  /v1/account/tenants:
    get:
      tags: [Account]
      summary: List tenants the authenticated user can access
      operationId: accountTenants
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Tenant list with role per tenant

  /v1/account/profile:
    get:
      tags: [Account]
      summary: Get the current user's profile
      operationId: accountGetProfile
      security:
        - BearerAuth: []
      responses:
        "200":
          description: User profile
    put:
      tags: [Account]
      summary: Update the current user's profile
      operationId: accountUpdateProfile
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "200":
          description: Profile updated

  /v1/account/change-password:
    post:
      tags: [Account]
      summary: Change the current user's password
      operationId: accountChangePassword
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [current_password, password, password_confirmation]
              properties:
                current_password:
                  type: string
                password:
                  type: string
                password_confirmation:
                  type: string
      responses:
        "200":
          description: Password changed

  /v1/account/email/change:
    post:
      tags: [Account]
      summary: Request an email change (sends verification link to new address)
      operationId: accountRequestEmailChange
      security:
        - BearerAuth: []
      description: |
        Stores the new address as pending and emails a signed verification URL
        that expires in 48 hours. The old address remains active until the link
        is confirmed. Re-requesting overwrites any outstanding pending change.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [new_email, current_password]
              properties:
                new_email:
                  type: string
                  format: email
                current_password:
                  type: string
      responses:
        "200":
          description: Verification link sent to the new address

  /v1/account/email/confirm-change:
    post:
      tags: [Account]
      summary: Confirm a pending email change via signed link
      operationId: accountConfirmEmailChange
      description: |
        Called by the SPA after the user opens the signed verification link.
        Must include the signed URL parameters (id, hash, expires, signature)
        as query-string params — not body — so Laravel's signature check passes.
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: hash
          in: query
          required: true
          schema:
            type: string
        - name: expires
          in: query
          required: true
          schema:
            type: integer
        - name: signature
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Email updated
        "403":
          description: Expired or invalid signature (ACCOUNT.EMAIL_CHANGE_EXPIRED / ACCOUNT.EMAIL_CHANGE_INVALID)

  /v1/account/email/cancel-change:
    post:
      tags: [Account]
      summary: Cancel a pending email change
      operationId: accountCancelEmailChange
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Pending change cleared

  /v1/account/two-factor:
    get:
      tags: [Account]
      summary: Get current 2FA status
      operationId: accountTwoFactorStatus
      security:
        - BearerAuth: []
      responses:
        "200":
          description: "`{ enabled: boolean, confirmed_at: datetime|null }`"

  /v1/account/two-factor/enable:
    post:
      tags: [Account]
      summary: Start 2FA enrollment (returns QR code + secret)
      operationId: accountTwoFactorEnable
      security:
        - BearerAuth: []
      description: |
        Generates a TOTP secret and returns an `otpauth://` QR code (SVG) plus
        the raw secret. The secret is not persisted until `/two-factor/confirm`
        is called with a valid 6-digit code.
      responses:
        "200":
          description: QR code + secret (single-use; shown once)

  /v1/account/two-factor/confirm:
    post:
      tags: [Account]
      summary: Confirm and activate 2FA enrollment
      operationId: accountTwoFactorConfirm
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [totp_code]
              properties:
                totp_code:
                  type: string
                  description: 6-digit code from the authenticator app
      responses:
        "200":
          description: 2FA active; returns 8 single-use recovery codes (shown once)
        "422":
          description: Invalid code

  /v1/account/two-factor/disable:
    post:
      tags: [Account]
      summary: Disable 2FA on the current account
      operationId: accountTwoFactorDisable
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [current_password, totp_code]
              properties:
                current_password:
                  type: string
                totp_code:
                  type: string
      responses:
        "200":
          description: 2FA disabled

  /v1/account/two-factor/recovery-codes:
    post:
      tags: [Account]
      summary: Regenerate recovery codes
      operationId: accountTwoFactorRegenerateRecoveryCodes
      security:
        - BearerAuth: []
      description: Invalidates any unused recovery codes and issues 8 new ones.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [current_password, totp_code]
              properties:
                current_password:
                  type: string
                totp_code:
                  type: string
      responses:
        "200":
          description: New recovery codes (shown once)

  # ── Account (GDPR) ──────────────────────────────────────────────
  /v1/account/data-export:
    get:
      tags: [Account]
      summary: Export all personal data (GDPR Art. 15/20)
      operationId: exportData
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Full JSON export of user data

  /v1/account/delete:
    post:
      tags: [Account]
      summary: Request account deletion (GDPR Art. 17)
      operationId: deleteAccount
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirm, current_password]
              properties:
                confirm:
                  type: string
                  enum: [DELETE]
                current_password:
                  type: string
                  description: User's current login password. Required as a fresh-credential check.
                code:
                  type: string
                  description: TOTP code (6 digits). Optional; required when 2FA is enabled on the account. Must be exactly 6 digits if provided — a malformed value is rejected (422 VALIDATION.FAILED) even when 2FA is disabled.
                recovery_code:
                  type: string
                  description: 2FA recovery code (alternative to `code`). Format `XXXXX-XXXXX` (11 chars). Optional; required when 2FA is enabled on the account if `code` is not provided. Consumed on use. Same shape-validation as `code` — a malformed value is rejected (422 VALIDATION.FAILED) even when 2FA is disabled.
      responses:
        "200":
          description: Deletion scheduled (30-day grace period)
        "422":
          description: Validation or auth failure (ACCOUNT.PASSWORD_INCORRECT, ACCOUNT.TWO_FACTOR_INVALID_CODE)

  /v1/account/cancel-deletion:
    post:
      tags: [Account]
      summary: Cancel pending account deletion
      operationId: cancelDeletion
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Deletion cancelled, account reactivated

  # ── Marketing ────────────────────────────────────────────────────
  /v1/marketing/subscribe:
    post:
      tags: [Marketing]
      summary: Subscribe to newsletter (double opt-in)
      operationId: newsletterSubscribe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "201":
          description: Confirmation email sent

  /v1/marketing/unsubscribe:
    post:
      tags: [Marketing]
      summary: Unsubscribe from newsletter
      operationId: newsletterUnsubscribe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Unsubscribed

  /v1/marketing/confirm:
    get:
      tags: [Marketing]
      summary: Confirm newsletter subscription (double opt-in)
      operationId: newsletterConfirm
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Subscription confirmed
        "422":
          description: Invalid or expired confirmation token

  /v1/marketing/contact:
    post:
      tags: [Marketing]
      summary: Submit the public contact form
      operationId: contactForm
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, message]
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
                company:
                  type: string
                message:
                  type: string
      responses:
        "201":
          description: Message received

  /v1/consent/record:
    post:
      tags: [Marketing]
      summary: Record a GDPR consent event (server-side audit trail)
      operationId: recordConsent
      description: |
        Stores a consent decision (grant or revoke) against a policy version
        with client IP + user agent, so the audit trail satisfies GDPR Art. 7
        proof-of-consent requirements.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [policy, version, granted]
              properties:
                policy:
                  type: string
                  example: cookies
                version:
                  type: string
                  example: "2026-03-01"
                granted:
                  type: boolean
                categories:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Consent recorded

  # ── Monitoring ───────────────────────────────────────────────────
  /health:
    get:
      tags: [Monitoring]
      summary: Basic health check (DB + cache)
      operationId: healthBasic
      responses:
        "200":
          description: Healthy
        "503":
          description: Degraded

  /health/detailed:
    get:
      tags: [Monitoring]
      summary: Detailed health check (admin-only)
      operationId: healthDetailed
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Full component health breakdown

  /metrics:
    get:
      tags: [Monitoring]
      summary: Prometheus metrics
      operationId: metrics
      responses:
        "200":
          description: Prometheus text exposition format
          content:
            text/plain:
              schema:
                type: string

  # ── Tenant Management API ────────────────────────────────────────
  # All /v1/tenant/* endpoints require Bearer token with 'tenant' ability.
  # The token resolves to a single active tenant; when the authenticated
  # user belongs to multiple tenants, the request must include the
  # X-Tenant-ID header to disambiguate.
  /v1/tenant/products:
    get:
      tags: [Tenant]
      summary: List catalog products
      operationId: listTenantProducts
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Products list
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Product"
                  - $ref: "#/components/schemas/SuccessEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/tenant/products/{id}:
    get:
      tags: [Tenant]
      summary: Get a single product
      operationId: showTenantProduct
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Product
        "404":
          description: CATALOG.INVALID_PRODUCT

  /v1/tenant/licenses:
    get:
      tags: [Tenant]
      summary: List licenses
      operationId: listTenantLicenses
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Licenses list
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/License"
                  - $ref: "#/components/schemas/SuccessEnvelope"
    post:
      tags: [Tenant]
      summary: Issue a new license
      operationId: createTenantLicense
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LicenseCreateRequest"
      responses:
        "201":
          description: License created
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/License"
                  - $ref: "#/components/schemas/SuccessEnvelope"
        "422":
          description: Validation error

  /v1/tenant/licenses/{id}:
    get:
      tags: [Tenant]
      summary: Get a single license
      operationId: showTenantLicense
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: License
        "404":
          description: LICENSE.NOT_FOUND
    put:
      tags: [Tenant]
      summary: Update a license
      operationId: updateTenantLicense
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LicenseUpdateRequest"
      responses:
        "200":
          description: License updated
        "404":
          description: LICENSE.NOT_FOUND
        "422":
          description: Validation error
    delete:
      tags: [Tenant]
      summary: Delete a license
      operationId: deleteTenantLicense
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: License deleted
        "404":
          description: LICENSE.NOT_FOUND

  /v1/tenant/activations:
    get:
      tags: [Tenant]
      summary: List activations across all licenses
      operationId: listTenantActivations
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Activations list
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Activation"
                  - $ref: "#/components/schemas/SuccessEnvelope"

  /v1/tenant/entitlements:
    get:
      tags: [Tenant]
      summary: List computed entitlements for the active subscription
      operationId: listTenantEntitlements
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Entitlements list (empty if no active subscription)

  /v1/tenant/api-keys:
    get:
      tags: [Tenant]
      summary: List API keys
      operationId: listTenantApiKeys
      description: |
        Returns all API keys issued to the tenant. The plaintext key is
        returned only once at creation time — subsequent list calls expose
        name, last-used timestamp, and revocation state only.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: API keys list
    post:
      tags: [Tenant]
      summary: Issue a new API key
      operationId: createTenantApiKey
      description: |
        Creates a new API key. The plaintext key is returned in
        `data.plaintext` and will not be retrievable again.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
      responses:
        "201":
          description: API key created (plaintext returned once)
        "422":
          description: Validation error

  /v1/tenant/api-keys/{id}:
    delete:
      tags: [Tenant]
      summary: Revoke an API key
      operationId: revokeTenantApiKey
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: API key revoked
        "404":
          description: AUTH.API_KEY_NOT_FOUND

  /v1/tenant/settings:
    get:
      tags: [Tenant]
      summary: Get tenant settings
      operationId: getTenantSettings
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Tenant settings
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/TenantSettings"
                  - $ref: "#/components/schemas/SuccessEnvelope"
    put:
      tags: [Tenant]
      summary: Update tenant settings
      operationId: updateTenantSettings
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                billing_email:
                  type: string
                  format: email
                billing_contact_name:
                  type: string
                  maxLength: 255
                company_name:
                  type: string
                  nullable: true
                  maxLength: 255
      responses:
        "200":
          description: Settings updated
        "422":
          description: Validation error

  /v1/tenant/audit-logs:
    get:
      tags: [Tenant]
      summary: List audit log entries
      operationId: listTenantAuditLogs
      security:
        - BearerAuth: []
      parameters:
        - name: event_type
          in: query
          schema:
            type: string
        - name: severity
          in: query
          schema:
            type: string
            enum: [info, warning, error, critical]
        - name: search
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - name: per_page
          in: query
          schema:
            type: integer
            default: 25
            maximum: 100
      responses:
        "200":
          description: Paginated audit logs

  /v1/tenant/webhooks:
    get:
      tags: [Tenant]
      summary: List webhook endpoints
      operationId: listTenantWebhooks
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Webhook endpoints (each with a delivery count)
    post:
      tags: [Tenant]
      summary: Register a webhook endpoint
      operationId: createTenantWebhook
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  maxLength: 500
                description:
                  type: string
                  nullable: true
                  maxLength: 255
      responses:
        "201":
          description: Webhook endpoint created
        "422":
          description: Validation error

  /v1/tenant/webhooks/{id}:
    put:
      tags: [Tenant]
      summary: Update a webhook endpoint
      operationId: updateTenantWebhook
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                description:
                  type: string
                  nullable: true
                status:
                  type: string
                  enum: [active, disabled]
      responses:
        "200":
          description: Webhook endpoint updated
        "404":
          description: Webhook endpoint not found
    delete:
      tags: [Tenant]
      summary: Delete a webhook endpoint
      operationId: deleteTenantWebhook
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Webhook endpoint deleted
        "404":
          description: Webhook endpoint not found

  /v1/tenant/webhook-deliveries:
    get:
      tags: [Tenant]
      summary: List webhook delivery attempts
      operationId: listTenantWebhookDeliveries
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, delivered, failed]
        - name: event_type
          in: query
          schema:
            type: string
        - name: endpoint_id
          in: query
          schema:
            type: string
            format: uuid
        - name: per_page
          in: query
          schema:
            type: integer
            default: 25
            maximum: 100
      responses:
        "200":
          description: Paginated delivery history (includes summary stats in meta)

  /v1/tenant/notifications:
    get:
      tags: [Tenant]
      summary: List in-app notifications
      operationId: listTenantNotifications
      security:
        - BearerAuth: []
      parameters:
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: Paginated notifications (meta.unread_count included)

  /v1/tenant/notifications/{id}/read:
    post:
      tags: [Tenant]
      summary: Mark a notification as read
      operationId: markTenantNotificationRead
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Read state applied (data.marked is true when this call flipped the state)

  /v1/tenant/notifications/read-all:
    post:
      tags: [Tenant]
      summary: Mark all in-app notifications as read
      operationId: markAllTenantNotificationsRead
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Number of notifications flipped (data.marked)

  /v1/tenant/dev-tools/snapshot:
    get:
      tags: [Tenant]
      summary: Developer Tools snapshot
      operationId: tenantDevToolsSnapshot
      description: |
        Returns a compact snapshot of the tenant's current state — license
        count, active activation count, and the 20 most recent usage events.
        Intended for the Dev Tools panel in the SPA dashboard.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Snapshot payload

  # ── Customer Billing Portal ──────────────────────────────────────
  # All /v1/billing/* endpoints require Bearer token with 'tenant' ability.
  /v1/billing/invoices:
    get:
      tags: [Billing]
      summary: List invoices for the active tenant
      operationId: listBillingInvoices
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Invoices list

  /v1/billing/invoices/{id}:
    get:
      tags: [Billing]
      summary: Get a single invoice
      operationId: showBillingInvoice
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Invoice
        "404":
          description: Invoice not found

  /v1/billing/invoices/{id}/pdf:
    get:
      tags: [Billing]
      summary: Download an invoice PDF
      operationId: downloadBillingInvoicePdf
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Invoice PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          description: Invoice not found

  /v1/billing/invoices/export:
    get:
      tags: [Billing]
      summary: Export invoices as CSV
      operationId: exportBillingInvoices
      security:
        - BearerAuth: []
      responses:
        "200":
          description: CSV of invoices
          content:
            text/csv:
              schema:
                type: string

  /v1/billing/subscription:
    get:
      tags: [Billing]
      summary: Get the active subscription
      operationId: showBillingSubscription
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Subscription (null if none)

  /v1/billing/payment-methods:
    get:
      tags: [Billing]
      summary: List payment methods (sanitised)
      operationId: listBillingPaymentMethods
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Payment methods list

  /v1/billing/billing-portal-url:
    get:
      tags: [Billing]
      summary: Generate a Stripe billing portal redirect URL
      operationId: getBillingPortalUrl
      description: |
        Returns a short-lived URL to the tenant's Stripe-hosted billing
        portal (update payment method, cancel subscription, etc.).
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Portal URL

  /v1/billing/history:
    get:
      tags: [Billing]
      summary: Get combined billing history (invoices + events)
      operationId: listBillingHistory
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Billing history

  /v1/billing/settings:
    get:
      tags: [Billing]
      summary: Get billing contact and address settings
      operationId: getBillingSettings
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Billing settings
    put:
      tags: [Billing]
      summary: Update billing contact and address settings
      operationId: updateBillingSettings
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                billing_email:
                  type: string
                  format: email
                billing_contact_name:
                  type: string
                company_name:
                  type: string
                  nullable: true
                billing_address_street:
                  type: string
                  nullable: true
                billing_address_city:
                  type: string
                  nullable: true
                billing_address_state:
                  type: string
                  nullable: true
                billing_address_postal_code:
                  type: string
                  nullable: true
                billing_address_country:
                  type: string
                  nullable: true
                  description: ISO 3166-1 alpha-2 country code
      responses:
        "200":
          description: Settings updated
        "422":
          description: Validation error

  /v1/billing/notifications:
    get:
      tags: [Billing]
      summary: Get billing email notification preferences
      operationId: getBillingNotificationPreferences
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Notification preferences
    put:
      tags: [Billing]
      summary: Update billing email notification preferences
      operationId: updateBillingNotificationPreferences
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                notify_invoice_issued:
                  type: boolean
                notify_payment_succeeded:
                  type: boolean
                notify_payment_failed:
                  type: boolean
                notify_subscription_changed:
                  type: boolean
      responses:
        "200":
          description: Preferences updated

  /v1/billing/activity:
    get:
      tags: [Billing]
      summary: List billing activity (paginated, filtered)
      operationId: listBillingActivity
      security:
        - BearerAuth: []
      parameters:
        - name: event_type
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - name: per_page
          in: query
          schema:
            type: integer
            default: 25
            maximum: 100
      responses:
        "200":
          description: Paginated billing activity

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Tenant API key for Integration API endpoints
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sanctum
      description: Bearer token from login or registration

  schemas:
    LicenseValidationResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            valid:
              type: boolean
            license:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                key:
                  type: string
                type:
                  type: string
                  enum: [perpetual, subscription, trial]
                status:
                  type: string
                  enum: [active, suspended, expired]
                expires_at:
                  type: string
                  format: date-time
                  nullable: true
                entitlements:
                  type: object
                  additionalProperties: true
                  description: 'Flexible key-value entitlements (e.g., {"plan": "pro", "domains": 5})'
                  example:
                    plan: pro
        meta:
          $ref: "#/components/schemas/Meta"

    SuccessEnvelope:
      type: object
      properties:
        data:
          type: object
        meta:
          $ref: "#/components/schemas/Meta"

    ErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: "RATE_LIMIT.EXCEEDED"
            message:
              type: string
            type:
              type: string
            status:
              type: integer
            details:
              type: object
        meta:
          $ref: "#/components/schemas/Meta"

    Meta:
      type: object
      properties:
        request_id:
          type: string
          format: uuid
        api_version:
          type: string
          example: "1"

    AdminProfile:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        created_at:
          type: string
          format: date-time

    Product:
      type: object
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
          example: "validonx.core"
        name:
          type: string
        description:
          type: string
          nullable: true
        active:
          type: boolean
        created_at:
          type: string
          format: date-time

    License:
      type: object
      properties:
        id:
          type: string
          format: uuid
        key:
          type: string
          description: The license key string (format "LK-...")
        type:
          type: string
          enum: [perpetual, subscription, trial]
        status:
          type: string
          enum: [active, suspended, revoked, expired]
        entitlements:
          type: object
          additionalProperties: true
          description: 'Flexible key-value entitlements (e.g., {"plan": "pro", "seats": 10})'
          nullable: true
        max_activations:
          type: integer
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        product_id:
          type: string
          format: uuid
          nullable: true
        plan_id:
          type: string
          format: uuid
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    LicenseCreateRequest:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [perpetual, subscription, trial]
        status:
          type: string
          enum: [active, suspended, revoked, expired]
        entitlements:
          type: object
          additionalProperties: true
        expires_at:
          type: string
          format: date-time
          description: Required to be in the future
        product_id:
          type: string
          format: uuid
        plan_id:
          type: string
          format: uuid
        max_activations:
          type: integer
          minimum: 1
          maximum: 100000
        key:
          type: string
          maxLength: 255
          description: Optional custom key; auto-generated when omitted

    LicenseUpdateRequest:
      type: object
      properties:
        type:
          type: string
          enum: [perpetual, subscription, trial]
        status:
          type: string
          enum: [active, suspended, revoked, expired]
        entitlements:
          type: object
          additionalProperties: true
        expires_at:
          type: string
          format: date-time
        max_activations:
          type: integer
          minimum: 1
          maximum: 100000

    Activation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        license_key_id:
          type: string
          format: uuid
        instance_id:
          type: string
        device_fingerprint:
          type: string
        status:
          type: string
          enum: [active, suspended, revoked]
        activated_at:
          type: string
          format: date-time
          nullable: true
        deactivated_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    ApiKey:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
          description: First few characters of the key for display only
        last_used_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    TenantSettings:
      type: object
      properties:
        name:
          type: string
        code:
          type: string
        status:
          type: string
          enum: [active, suspended, provisioning, closed]
        billing_email:
          type: string
          format: email
          nullable: true
        billing_contact_name:
          type: string
          nullable: true
        company_name:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

  responses:
    Unauthorized:
      description: Invalid or missing authentication
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
