openapi: 3.1.0
info:
  title: BATbern Companies API
  version: 1.0.0
  description: |
    Consolidated Companies API for BATbern Event Management Platform.

    This API provides comprehensive company management capabilities with:
    - **Resource Expansion**: `?include=` parameter for statistics and logos
    - **Advanced Filtering**: MongoDB-style JSON filter syntax for complex queries
    - **Field Selection**: Sparse fieldsets to reduce payload size
    - **Pagination**: Standard page/limit parameters
    - **Caching**: Caffeine in-memory cache (15min TTL)
    - **Swiss UID Validation**: Validates Swiss company registration numbers

    **Story**: 1.14 - Company Management Service Foundation
    **Story 1.16.2**: Eliminate UUIDs from API - Use company name as identifier

    **Performance Targets**:
    - Company list (no includes): <100ms (P95)
    - Company detail (basic): <150ms (P95)
    - Company detail (all includes): <200ms (P95)
    - Cached search response: <50ms (P95)
    - Advanced query with all parameters: <200ms (P95)

  contact:
    name: BATbern Platform Team
    email: platform@batbern.ch
  license:
    name: Proprietary
    identifier: Proprietary

servers:
  - url: https://api.batbern.ch/api/v1
    description: Production
  - url: https://api.batbern.ch/api/v1
    description: Staging
  - url: http://localhost:8080/api/v1
    description: Development

tags:
  - name: Companies
    description: Company CRUD and listing operations
  - name: Company Search
    description: Company search and autocomplete with caching
  - name: Company Verification
    description: Company verification and UID validation
  - name: Presentation Settings
    description: Moderator presentation page settings (Story 10.8a)
  - name: Logos
    description: Generic logo/image upload (presigned-URL flow, URL proxy/import, admin cleanup) — LogoController, Story 1.16.3

paths:
  /logos/presigned-url:
    post:
      tags:
        - Logos
      summary: Generate a presigned URL for a logo upload
      description: |
        Phase 1 of the three-phase upload flow. Returns a presigned S3 URL (15-min expiry) plus
        upload metadata. Supports PNG/JPEG/SVG up to 5MB. No authentication required.
      operationId: requestLogoUploadUrl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LogoUploadRequest'
      responses:
        '200':
          description: Presigned URL generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogoPresignedUploadUrl'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /logos/{uploadId}/confirm:
    post:
      tags:
        - Logos
      summary: Confirm a logo upload completion
      description: |
        Phase 2 — confirms the file was uploaded to S3 (PENDING → CONFIRMED). No authentication required.
      operationId: confirmLogoUpload
      parameters:
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LogoUploadConfirmRequest'
      responses:
        '200':
          description: Upload confirmed
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /logos/{uploadId}:
    delete:
      tags:
        - Logos
      summary: Delete an unused logo
      description: |
        Deletes a logo in PENDING or CONFIRMED status (ASSOCIATED logos cannot be deleted).
        Requires authentication.
      operationId: deleteUnusedLogo
      parameters:
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Logo deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - BearerAuth: []

  /logos/cleanup/statistics:
    get:
      tags:
        - Logos
      summary: Get logo-upload cleanup statistics (Admin)
      description: Counts of logo uploads by state, for monitoring orphaned uploads. Requires ORGANIZER role.
      operationId: getLogoCleanupStatistics
      responses:
        '200':
          description: Cleanup statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogoCleanupStatistics'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - BearerAuth: []

  /logos/cleanup/trigger:
    post:
      tags:
        - Logos
      summary: Manually trigger the logo cleanup job (Admin)
      description: Triggers the orphaned-upload cleanup job (normally runs daily at 02:00). Requires ORGANIZER role.
      operationId: triggerLogoCleanup
      responses:
        '202':
          description: Cleanup job triggered
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - BearerAuth: []

  /logos/fetch-from-url:
    post:
      tags:
        - Logos
      summary: Fetch an image from an external URL (CORS proxy)
      description: |
        Fetches an image from an external URL and returns the raw bytes with the appropriate
        content type. Proxies around CORS for logo import. Up to 10MB; validates the content is
        an image. No authentication required.
      operationId: fetchImageFromUrl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FetchImageFromUrlRequest'
      responses:
        '200':
          description: Image bytes
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /logos/upload-from-url:
    post:
      tags:
        - Logos
      summary: Fetch an image from a URL and upload it to S3 (server-side)
      description: |
        Fetches an image from a URL and uploads it directly to S3 (bypasses the browser to avoid
        binary corruption), returning the upload ID. Up to 10MB. No authentication required.
      operationId: uploadImageFromUrl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadImageFromUrlRequest'
      responses:
        '200':
          description: Image uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadImageFromUrlResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          description: Image exceeds the 10MB limit
        '500':
          $ref: '#/components/responses/InternalServerError'

  /companies:
    get:
      tags:
        - Companies
      summary: List and search companies with advanced query support
      description: |
        Retrieve a paginated list of companies with optional filtering, sorting, field selection, and resource expansion.

        **Acceptance Criteria**: AC1, AC14, AC15

        **Filter Syntax Examples**:
        - Single filter: `{"industry":"Technology"}`
        - Multiple fields: `{"industry":"Technology","isVerified":true}`
        - Logical operators: `{"$or":[{"industry":"Technology"},{"industry":"Finance"}]}`
        - Comparison: `{"createdAt":{"$gte":"2025-01-01T00:00:00Z"}}`
        - Contains: `{"name":{"$contains":"Bern"}}`

        **Sort Syntax**:
        - Ascending: `name` or `+name`
        - Descending: `-name`
        - Multiple fields: `industry,-createdAt`

        **Field Selection**:
        - Specific fields: `?fields=id,name,industry`
        - All fields: omit `fields` parameter

        **Resource Expansion**:
        - Statistics: `?include=statistics` (totalEvents, totalSpeakers, totalPartners)
        - Logo: `?include=logo` (url, s3Key, fileId)
        - Multiple: `?include=statistics,logo`

        **Performance**:
        - Basic query: <100ms (P95)
        - With all includes: <200ms (P95)

      operationId: listCompanies
      parameters:
        - name: filter
          in: query
          description: MongoDB-style JSON filter criteria
          required: false
          schema:
            type: string
            example: '{"industry":"Technology","isVerified":true}'
        - name: sort
          in: query
          description: Sort fields (comma-separated, prefix with - for descending)
          required: false
          schema:
            type: string
            example: '-createdAt,name'
        - name: page
          in: query
          description: Page number (1-indexed)
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: Items per page (max 100)
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: fields
          in: query
          description: Comma-separated field names for sparse fieldsets
          required: false
          schema:
            type: string
            example: 'id,name,industry,isVerified'
        - name: include
          in: query
          description: Comma-separated list of resources to include
          required: false
          schema:
            type: string
            example: 'statistics,logo'
      responses:
        '200':
          description: Successful response with paginated companies
          headers:
            X-Cache-Status:
              description: Cache hit status (HIT or MISS)
              schema:
                type: string
                enum: [HIT, MISS]
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCompanyResponse'
              examples:
                basicList:
                  summary: Basic company list
                  value:
                    data:
                      - name: SwissITSolutionsAG
                        displayName: Swiss IT Solutions AG
                        industry: Technology
                        isVerified: true
                        createdAt: 2025-01-15T10:00:00Z
                        updatedAt: 2025-01-15T10:00:00Z
                    pagination:
                      page: 1
                      limit: 20
                      totalItems: 1
                      totalPages: 1
                      hasNext: false
                      hasPrev: false
                expandedList:
                  summary: Company list with statistics and logo
                  value:
                    data:
                      - name: SwissITSolutionsAG
                        displayName: Swiss IT Solutions AG
                        industry: Technology
                        isVerified: true
                        createdAt: 2025-01-15T10:00:00Z
                        updatedAt: 2025-01-15T10:00:00Z
                        statistics:
                          totalEvents: 12
                          totalSpeakers: 8
                          totalPartners: 5
                        logo:
                          url: https://cdn.batbern.ch/logos/swiss-it-solutions-ag/logo.png
                          s3Key: logos/swiss-it-solutions-ag/logo.png
                          fileId: file-123
                    pagination:
                      page: 1
                      limit: 20
                      totalItems: 1
                      totalPages: 1
                      hasNext: false
                      hasPrev: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags:
        - Companies
      summary: Create a new company
      description: |
        Create a new company with complete company data.

        **Acceptance Criteria**: AC1, AC3

        **Validation Rules**:
        - Company name must be unique
        - Swiss UID format (if provided): CHE-XXX.XXX.XXX
        - Website must be valid URL (if provided)

        **Events Published**: CompanyCreatedEvent to EventBridge

        **Performance**: <200ms (P95)

      operationId: createCompany
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCompanyRequest'
            examples:
              basicCompany:
                summary: Basic company creation
                value:
                  name: SwissITSolutionsAG
                  displayName: Swiss IT Solutions AG
                  industry: Technology
                  website: https://swiss-it.ch
                  description: Leading Swiss IT consulting firm
              withSwissUID:
                summary: Company with Swiss UID
                value:
                  name: Bern Tech GmbH
                  swissUID: CHE-123.456.789
                  industry: Technology
                  website: https://berntech.ch
      responses:
        '201':
          description: Company created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

  /companies/{name}:
    get:
      tags:
        - Companies
      summary: Get company details by name
      description: |
        Retrieve detailed information about a specific company.

        **Acceptance Criteria**: AC2

        **Story 1.16.2**: Uses company name as identifier instead of UUID

        **Performance**: <150ms (P95)

      operationId: getCompany
      parameters:
        - name: name
          in: path
          description: Company name (unique identifier)
          required: true
          schema:
            type: string
            example: SwissITSolutionsAG
      responses:
        '200':
          description: Company details retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse'
              examples:
                basicCompany:
                  summary: Basic company details
                  value:
                    name: SwissITSolutionsAG
                    displayName: Swiss IT Solutions AG
                    swissUID: CHE-123.456.789
                    industry: Technology
                    website: https://swiss-it.ch
                    description: Leading Swiss IT consulting firm
                    isVerified: true
                    createdAt: 2025-01-15T10:00:00Z
                    updatedAt: 2025-01-15T10:00:00Z
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

    patch:
      tags:
        - Companies
      summary: Partially update company
      description: |
        Update specific fields of an existing company.

        **Acceptance Criteria**: AC4

        **Story 1.16.2**: Uses company name as identifier instead of UUID

        **Validation Rules**:
        - Company name must be unique
        - Swiss UID format (if provided): CHE-XXX.XXX.XXX

        **Events Published**: CompanyUpdatedEvent to EventBridge

        **Cache Invalidation**: All company caches cleared on update

      operationId: patchCompany
      parameters:
        - name: name
          in: path
          description: Company name (unique identifier)
          required: true
          schema:
            type: string
            example: SwissITSolutionsAG
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCompanyRequest'
      responses:
        '200':
          description: Company updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

    delete:
      tags:
        - Companies
      summary: Delete company
      description: |
        Delete a company and all associated data.

        **Acceptance Criteria**: AC4

        **Story 1.16.2**: Uses company name as identifier instead of UUID

        **Events Published**: CompanyDeletedEvent to EventBridge

        **Cache Invalidation**: All company caches cleared on deletion

      operationId: deleteCompany
      parameters:
        - name: name
          in: path
          description: Company name (unique identifier)
          required: true
          schema:
            type: string
            example: SwissITSolutionsAG
      responses:
        '204':
          description: Company deleted successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

  /companies/search:
    get:
      tags:
        - Company Search
      summary: Search companies with autocomplete
      description: |
        Search companies by name with autocomplete functionality.

        **Acceptance Criteria**: AC5, AC11

        **Caching**:
        - Caffeine in-memory cache with 15-minute TTL
        - Cache key includes query, limit, and include parameters
        - Automatic cache invalidation on company updates

        **Performance**:
        - Cached response: <50ms (P95)
        - Cache miss: <100ms (P95)

        **Default Results**: 20 companies (configurable via limit parameter)

        **Resource Expansion**:
        - Logo: `?include=logo` (logoUrl field included in response)

      operationId: searchCompanies
      parameters:
        - name: query
          in: query
          description: Search query (minimum 1 character)
          required: true
          schema:
            type: string
            minLength: 1
            example: 'Swiss'
        - name: limit
          in: query
          description: Maximum number of results (default 20)
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: include
          in: query
          description: Comma-separated list of resources to include (e.g., logo)
          required: false
          schema:
            type: string
            example: 'logo'
      responses:
        '200':
          description: Search results returned successfully
          headers:
            X-Cache-Status:
              description: Cache hit status (HIT or MISS)
              schema:
                type: string
                enum: [HIT, MISS]
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CompanySearchResponse'
              examples:
                searchResults:
                  summary: Search results for "Swiss"
                  value:
                    - name: SwissITSolutionsAG
                      displayName: Swiss IT Solutions AG
                      swissUID: CHE-123.456.789
                      industry: Technology
                      isVerified: true
                    - name: Swiss Bank Corp
                      displayName: Swiss Bank
                      swissUID: CHE-987.654.321
                      industry: Finance
                      isVerified: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

  /companies/validate-uid:
    get:
      tags:
        - Company Verification
      summary: Validate Swiss UID format
      description: |
        Validates Swiss company UID (Unternehmens-Identifikationsnummer) format.

        **Acceptance Criteria**: AC12

        **Expected Format**: CHE-XXX.XXX.XXX

        **Validation Rules**:
        - Must start with "CHE-"
        - Followed by 9 digits in XXX.XXX.XXX format
        - Total length: 15 characters

        **Future Enhancement**: Integration with Swiss Business Registry for real-time validation

      operationId: validateUID
      parameters:
        - name: uid
          in: query
          description: Swiss UID to validate
          required: true
          schema:
            type: string
            # No `pattern` here: this endpoint's whole purpose is to report whether an arbitrary
            # input is a valid UID (returns {valid:false,...} for malformed input — see the
            # invalidUID example below). A @Pattern on the query param would 400 every malformed
            # UID before the handler runs, making valid:false unreachable and breaking the deployed
            # behaviour (the hand-rolled controller accepted any string). Format check lives in the
            # handler (SwissUIDValidationService), not in request validation.
            example: 'CHE-123.456.789'
      responses:
        '200':
          description: Validation result returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UIDValidationResponse'
              examples:
                validUID:
                  summary: Valid Swiss UID
                  value:
                    valid: true
                    uid: CHE-123.456.789
                    message: Valid Swiss UID format
                invalidUID:
                  summary: Invalid Swiss UID
                  value:
                    valid: false
                    uid: CHE-123456789
                    message: 'Invalid Swiss UID format. Expected: CHE-XXX.XXX.XXX'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

  /companies/{name}/verify:
    post:
      tags:
        - Company Verification
      summary: Verify company
      description: |
        Marks a company as verified by an ORGANIZER.

        **Acceptance Criteria**: AC13

        **Story 1.16.2**: Uses company name as identifier instead of UUID

        **Authorization**: ORGANIZER role required

        **Events Published**: CompanyVerifiedEvent to EventBridge

        **Idempotency**: Safe to call multiple times (no error if already verified)

      operationId: verifyCompany
      parameters:
        - name: name
          in: path
          description: Company name (unique identifier)
          required: true
          schema:
            type: string
            example: SwissITSolutionsAG
      responses:
        '200':
          description: Company verified successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

  /companies:get-or-create:
    post:
      tags:
        - Companies
      summary: Get or create company by display name
      description: |
        Gets an existing company or creates a new one from a display name.

        **ADR-003**: Uses slug generation to create meaningful company name (ID) from display name.

        **Story**: Anonymous user registration (ADR-005) - allows creating companies during anonymous registration.

        **Idempotency**: Safe to call multiple times - returns existing company if name slug already exists.

        **Slug Generation**:
        - Converts display name to lowercase alphanumeric slug (max 12 chars)
        - Example: "Test Co" → "testco", "Müller & Partners AG" → "muellerpart"
        - Handles German characters: ä→ae, ö→oe, ü→ue, ß→ss
        - Adds numeric suffix if slug collision occurs (e.g., "testco-2")

        **Authentication**: Supports both authenticated and anonymous contexts

        **Events Published**: CompanyCreatedEvent (only if new company created)

        **Performance**: <200ms (P95)

      operationId: getOrCreateCompany
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetOrCreateCompanyRequest'
            examples:
              basicCompany:
                summary: Get or create company from display name
                value:
                  displayName: Test Co
              germanCompany:
                summary: Company with German characters
                value:
                  displayName: Müller & Söhne AG
      responses:
        '200':
          description: Company retrieved or created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse'
              examples:
                existingCompany:
                  summary: Existing company returned
                  value:
                    name: testco
                    displayName: Test Co
                    isVerified: false
                    createdAt: 2025-01-01T10:00:00Z
                    updatedAt: 2025-01-01T10:00:00Z
                    createdBy: john.doe
                newCompany:
                  summary: New company created
                  value:
                    name: testco
                    displayName: Test Co
                    isVerified: false
                    createdAt: 2025-01-15T10:00:00Z
                    updatedAt: 2025-01-15T10:00:00Z
                    createdBy: anonymous
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # Logo upload endpoints removed - Story 1.16.3: Generic File Upload Service
  # Use the generic file upload API instead:
  # 1. POST /api/v1/logos/presigned-url - Generate upload URL
  # 2. PUT (presigned URL) - Upload file to S3
  # 3. POST /api/v1/logos/{uploadId}/confirm - Confirm upload
  # 4. POST /api/v1/companies (with logoUploadId) - Associate logo with company
  #
  # The logo upload operations are documented inline under the `Logos` tag in this spec
  # (served by LogoController -> generated LogosApi).

  /public/settings/presentation:
    get:
      tags:
        - Presentation Settings
      summary: Get presentation settings (public)
      description: |
        Returns the moderator presentation page settings.

        **Story 10.8a**: Moderator Presentation Page — Functional

        **Security**: No authentication required — public endpoint.

      operationId: getPresentationSettings
      responses:
        '200':
          description: Presentation settings returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresentationSettingsResponse'
              examples:
                defaults:
                  summary: Default settings
                  value:
                    aboutText: BATbern ist eine unabhängige Plattform für Berner Architekten Treffen.
                    partnerCount: 9
        '500':
          $ref: '#/components/responses/InternalServerError'

  /settings/presentation:
    put:
      tags:
        - Presentation Settings
      summary: Update presentation settings
      description: |
        Updates the moderator presentation page settings.

        **Story 10.8a**: Moderator Presentation Page — Functional

        **Authorization**: ORGANIZER role required.

      operationId: updatePresentationSettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PresentationSettingsRequest'
            examples:
              update:
                summary: Update settings
                value:
                  aboutText: BATbern ist eine unabhängige Plattform für Berner Architekten Treffen.
                  partnerCount: 12
      responses:
        '200':
          description: Presentation settings updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresentationSettingsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - BearerAuth: []

components:
  schemas:
    LogoUploadRequest:
      type: object
      required:
        - fileName
        - fileSize
        - mimeType
      properties:
        fileName:
          type: string
          description: Original filename with extension (e.g. "company-logo.png")
        fileSize:
          type: integer
          format: int64
          description: File size in bytes (max 5,242,880 = 5MB)
        mimeType:
          type: string
          description: MIME type (e.g. "image/png", "image/jpeg", "image/svg+xml")
    LogoPresignedUploadUrl:
      type: object
      description: Presigned S3 upload URL + metadata for a logo upload
      properties:
        uploadUrl:
          type: string
          description: The presigned S3 URL to PUT the file to
        fileId:
          type: string
          description: Unique file identifier for tracking the upload
        s3Key:
          type: string
          description: S3 key where the file will be stored
        fileExtension:
          type: string
          description: File extension (used to reconstruct the S3 key on confirm)
        expiresInMinutes:
          type: integer
          description: URL expiry in minutes (typically 15)
        requiredHeaders:
          type: object
          additionalProperties:
            type: string
          description: Headers the client must send with the S3 PUT
    LogoUploadConfirmRequest:
      type: object
      required:
        - fileId
        - fileExtension
      properties:
        fileId:
          type: string
          description: File ID returned from the presigned-URL request
        fileExtension:
          type: string
          description: File extension (used to reconstruct the S3 key)
        checksum:
          type: string
          description: Optional SHA-256 checksum for integrity verification
    LogoCleanupStatistics:
      type: object
      description: Counts of logo uploads by state (admin monitoring)
      properties:
        pendingCount:
          type: integer
          format: int64
        confirmedCount:
          type: integer
          format: int64
        associatedCount:
          type: integer
          format: int64
        expiredPendingCount:
          type: integer
          format: int64
        expiredConfirmedCount:
          type: integer
          format: int64
    FetchImageFromUrlRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          description: External image URL to fetch
    UploadImageFromUrlRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          description: External image URL to fetch and upload
        filename:
          type: string
          description: Suggested base filename (extension derived from content type)
          default: logo
    UploadImageFromUrlResponse:
      type: object
      properties:
        uploadId:
          type: string
          description: Upload ID for use in subsequent company creation
    PresentationSettingsResponse:
      type: object
      description: Moderator presentation page settings (Story 10.8a)
      required:
        - aboutText
        - partnerCount
      properties:
        aboutText:
          type: string
          description: Verein purpose text displayed on the About slide
          example: BATbern ist eine unabhängige Plattform für Berner Architekten Treffen.
        partnerCount:
          type: integer
          description: Number of active partner companies shown on About slide
          minimum: 0
          example: 9

    PresentationSettingsRequest:
      type: object
      description: Request body for updating moderator presentation page settings (Story 10.8a)
      required:
        - aboutText
        - partnerCount
      properties:
        aboutText:
          type: string
          description: Verein purpose text displayed on the About slide
          minLength: 1
          example: BATbern ist eine unabhängige Plattform für Berner Architekten Treffen.
        partnerCount:
          type: integer
          description: Number of active partner companies shown on About slide
          minimum: 0
          example: 9

    CompanyResponse:
      type: object
      description: |
        Company response with meaningful IDs.
        Story 1.16.2: name field is the unique identifier (no separate id field).
      required:
        - name
        - isVerified
        - createdAt
        - updatedAt
      properties:
        name:
          type: string
          description: Company name (unique identifier, alphanumeric only) - Story 1.16.2
          pattern: '^[A-Za-z0-9]+$'
          maxLength: 255
          example: SwissITSolutionsAG
        displayName:
          type: string
          maxLength: 255
          example: Swiss IT Solutions AG
        swissUID:
          type: string
          pattern: '^CHE-\d{3}\.\d{3}\.\d{3}$'
          example: CHE-123.456.789
        website:
          # Modelled as a plain string (NOT `format: uri`) so the Spring generator's Bean
          # Validation @Size(max=500) applies cleanly — @Size has no validator for java.net.URI,
          # so `format: uri` + maxLength yields `@Size URI` → UnexpectedTypeException (500) on every
          # @Valid request body carrying a website. Mirrors users-api profilePictureUrl (ADR-013).
          type: string
          maxLength: 500
          example: https://swiss-it.ch
        industry:
          type: string
          maxLength: 100
          example: Technology
        description:
          type: string
          maxLength: 2000
          example: Leading Swiss IT consulting firm specializing in cloud-native architectures
        isVerified:
          type: boolean
          example: true
        createdAt:
          type: string
          format: date-time
          example: 2025-01-15T10:00:00Z
        updatedAt:
          type: string
          format: date-time
          example: 2025-01-15T10:00:00Z
        createdBy:
          type: string
          description: Username of the user who created this company (Story 1.16.2)
          example: john.doe
          pattern: '^[a-z]+\.[a-z]+(\.[0-9]+)?$'
        statistics:
          $ref: '#/components/schemas/CompanyStatistics'
        logo:
          $ref: '#/components/schemas/CompanyLogo'

    CompanyStatistics:
      type: object
      description: Company statistics (included when ?include=statistics)
      properties:
        totalEvents:
          type: integer
          description: Total number of events the company participated in
          example: 12
        totalSpeakers:
          type: integer
          description: Total number of speakers from this company
          example: 8
        totalPartners:
          type: integer
          description: Total number of partnership engagements
          example: 5

    CompanyLogo:
      type: object
      description: Company logo details (included when ?include=logo)
      properties:
        url:
          type: string
          format: uri
          description: CloudFront CDN URL for logo
          example: https://cdn.batbern.ch/logos/swiss-it-solutions-ag/logo.png
        s3Key:
          type: string
          description: S3 object key
          example: logos/swiss-it-solutions-ag/logo.png
        fileId:
          type: string
          description: Internal file identifier
          example: file-123

    # Logo upload is served by LogoController under the `Logos` tag in this same spec
    # (consolidated from the retired file-upload-api spec — API consolidation 2026-06-30).

    CompanySearchResponse:
      type: object
      description: |
        Company search result with meaningful IDs.
        Story 1.16.2: name field is the unique identifier (no separate id field).
      required:
        - name
        - isVerified
      properties:
        name:
          type: string
          description: Company name (unique identifier, alphanumeric only) - Story 1.16.2
          pattern: '^[A-Za-z0-9]+$'
          example: SwissITSolutionsAG
        displayName:
          type: string
          example: Swiss IT Solutions AG
        swissUID:
          type: string
          example: CHE-123.456.789
        industry:
          type: string
          example: Technology
        isVerified:
          type: boolean
          example: true
        logo:
          $ref: '#/components/schemas/CompanyLogo'

    CreateCompanyRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          # No `^[A-Za-z0-9]+$` pattern: the deployed CompanyController has always validated name
          # as @Size(2..255) only (spaces allowed, e.g. "Swisscom AG"). The alphanumeric pattern was
          # contract drift never enforced by the hand-rolled controller — applying it now (Phase 7
          # contract-first wiring) would 400 real requests. Relaxed to match production behaviour.
          description: Company name (unique identifier; 2-255 chars)
          minLength: 2
          maxLength: 255
          example: Swisscom AG
        displayName:
          type: string
          maxLength: 255
          example: Swiss IT Solutions AG
        swissUID:
          type: string
          pattern: '^CHE-\d{3}\.\d{3}\.\d{3}$'
          example: CHE-123.456.789
        website:
          # Modelled as a plain string (NOT `format: uri`) so the Spring generator's Bean
          # Validation @Size(max=500) applies cleanly — @Size has no validator for java.net.URI,
          # so `format: uri` + maxLength yields `@Size URI` → UnexpectedTypeException (500) on every
          # @Valid request body carrying a website. Mirrors users-api profilePictureUrl (ADR-013).
          type: string
          maxLength: 500
          example: https://swiss-it.ch
        industry:
          type: string
          maxLength: 100
          example: Technology
        description:
          type: string
          maxLength: 2000
          example: Leading Swiss IT consulting firm
        logoUploadId:
          type: string
          description: Upload ID from three-phase upload via /api/v1/logos/presigned-url (optional)
          example: abc123-def456

    UpdateCompanyRequest:
      type: object
      properties:
        name:
          type: string
          # See CreateCompanyRequest.name: the alphanumeric pattern was never enforced by the
          # deployed controller (@Size only); relaxed to match production behaviour (Phase 7).
          description: Company name (unique identifier; 2-255 chars)
          minLength: 2
          maxLength: 255
          example: Swisscom AG
        displayName:
          type: string
          maxLength: 255
          example: Swiss IT Solutions AG
        swissUID:
          type: string
          pattern: '^CHE-\d{3}\.\d{3}\.\d{3}$'
          example: CHE-123.456.789
        website:
          # Modelled as a plain string (NOT `format: uri`) so the Spring generator's Bean
          # Validation @Size(max=500) applies cleanly — @Size has no validator for java.net.URI,
          # so `format: uri` + maxLength yields `@Size URI` → UnexpectedTypeException (500) on every
          # @Valid request body carrying a website. Mirrors users-api profilePictureUrl (ADR-013).
          type: string
          maxLength: 500
          example: https://swiss-it.ch
        industry:
          type: string
          maxLength: 100
          example: Technology
        description:
          type: string
          maxLength: 2000
          example: Leading Swiss IT consulting firm
        logoUploadId:
          type: string
          description: Upload ID from three-phase upload (set to empty string to remove logo)
          example: abc123-def456

    GetOrCreateCompanyRequest:
      type: object
      description: |
        Request to get or create a company by display name.
        The system will generate a slug from the display name and either return the existing company or create a new one.
      required:
        - displayName
      properties:
        displayName:
          type: string
          description: Full company display name (will be converted to slug for company.name)
          minLength: 1
          maxLength: 255
          example: Test Co

    UIDValidationResponse:
      type: object
      required:
        - valid
        - uid
        - message
      properties:
        valid:
          type: boolean
          example: true
        uid:
          type: string
          example: CHE-123.456.789
        message:
          type: string
          example: Valid Swiss UID format

    PaginatedCompanyResponse:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/CompanyResponse'
        pagination:
          $ref: '#/components/schemas/PaginationMetadata'

    PaginationMetadata:
      $ref: './_shared.openapi.yml#/components/schemas/PaginationMetadata'
    ErrorResponse:
      $ref: './_shared.openapi.yml#/components/schemas/ErrorResponse'
  responses:
    BadRequest:
      description: Bad request - validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            validationError:
              value:
                error: VALIDATION_ERROR
                errorCode: BAD_REQUEST
                message: Invalid filter syntax
                timestamp: 2025-01-15T10:30:00Z
            invalidUID:
              value:
                error: VALIDATION_ERROR
                errorCode: BAD_REQUEST
                message: Invalid Swiss UID format
                timestamp: 2025-01-15T10:30:00Z

    Unauthorized:
      description: Unauthorized - missing or invalid JWT token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missingToken:
              value:
                error: UNAUTHORIZED
                errorCode: UNAUTHORIZED
                message: Missing or invalid authentication token
                timestamp: 2025-01-15T10:30:00Z

    Forbidden:
      description: Forbidden - insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            insufficientPermissions:
              value:
                error: FORBIDDEN
                errorCode: FORBIDDEN
                message: Insufficient permissions. Requires ORGANIZER role.
                timestamp: 2025-01-15T10:30:00Z

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            companyNotFound:
              value:
                error: RESOURCE_NOT_FOUND
                errorCode: NOT_FOUND
                message: Company not found with name 'SwissITSolutionsAG'
                timestamp: 2025-01-15T10:30:00Z

    Conflict:
      description: Conflict - resource already exists
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            duplicateName:
              value:
                error: CONFLICT
                errorCode: CONFLICT
                message: Company with name 'SwissITSolutionsAG' already exists
                timestamp: 2025-01-15T10:30:00Z

    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            serverError:
              value:
                error: INTERNAL_SERVER_ERROR
                errorCode: INTERNAL_ERROR
                message: An unexpected error occurred
                timestamp: 2025-01-15T10:30:00Z

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: AWS Cognito JWT token

security:
  - BearerAuth: []
