openapi: 3.1.0
info:
  title: App APEU API
  version: 1.1.0
  description: Contrato inicial da plataforma APEU. Dados individuais da planilha recebida não são publicados pelo protótipo.
servers:
  - url: https://api.apeurn.com/v1
    description: Produção proposta
security:
  - oauth2: [openid]
tags:
  - name: Membership
  - name: Benefits
  - name: Finance
  - name: Transport
  - name: Meetings
  - name: Elections
  - name: Documents
  - name: Public
paths:
  /membership-applications:
    post:
      tags: [Membership]
      summary: Envia solicitação de filiação
      operationId: createMembershipApplication
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MembershipApplicationInput' }
      responses:
        '201':
          description: Solicitação recebida
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MembershipApplication' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /admin/membership-applications/{id}:
    patch:
      tags: [Membership]
      summary: Decide solicitação de filiação
      operationId: decideMembershipApplication
      security: [{ oauth2: [admin:members] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision: { type: string, enum: [approved, rejected, correction_requested] }
                reason: { type: string, maxLength: 1000 }
      responses:
        '200': { description: Decisão registrada }
        '409': { $ref: '#/components/responses/Conflict' }
  /notices:
    get:
      tags: [Benefits]
      summary: Lista editais visíveis
      operationId: listNotices
      parameters:
        - in: query
          name: status
          schema: { type: string }
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Página de editais
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NoticePage' }
  /notices/{id}/applications:
    post:
      tags: [Benefits]
      summary: Inscreve associado em edital
      operationId: createBenefitApplication
      security: [{ oauth2: [member:benefits] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '201': { description: Inscrição criada }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /applications/{id}/appeals:
    post:
      tags: [Benefits]
      summary: Registra recurso administrativo
      operationId: createAppeal
      security: [{ oauth2: [member:benefits] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '201': { description: Recurso recebido }
        '409': { description: Prazo encerrado ou recurso já existente }
  /admin/member-imports/validate:
    post:
      tags: [Membership]
      summary: Valida planilha de associados sem persistir registros
      operationId: validateMemberImport
      security: [{ oauth2: [admin:imports] }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        '200':
          description: Diagnóstico agregado, sem retornar CPF ou matrícula
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MemberImportDiagnostic' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /finance/fee-policy/current:
    get:
      tags: [Finance]
      summary: Retorna a regra e o valor mensal vigentes
      operationId: getCurrentFeePolicy
      responses:
        '200':
          description: Mensalidade calculada a partir do salário mínimo vigente
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeePolicy' }
  /admin/finance/minimum-wage-periods:
    post:
      tags: [Finance]
      summary: Registra nova vigência oficial do salário mínimo
      operationId: createMinimumWagePeriod
      security: [{ oauth2: [admin:finance] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MinimumWagePeriodInput' }
      responses:
        '201':
          description: Vigência registrada para dupla aprovação
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /charges/{id}/payment-intents:
    post:
      tags: [Finance]
      summary: Cria intenção de pagamento
      operationId: createPaymentIntent
      security: [{ oauth2: [member:finance] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [method]
              properties:
                method: { type: string, enum: [pix, boleto, card] }
      responses:
        '201': { description: Intenção criada }
        '409': { $ref: '#/components/responses/Conflict' }
  /payments/webhooks/{provider}:
    post:
      tags: [Finance]
      summary: Recebe webhook assinado do gateway
      operationId: processPaymentWebhook
      security: []
      parameters:
        - in: path
          name: provider
          required: true
          schema: { type: string }
      responses:
        '204': { description: Evento processado ou já conhecido }
        '401': { description: Assinatura inválida }
  /trips:
    get:
      tags: [Transport]
      summary: Lista viagens disponíveis
      operationId: listTrips
      security: [{ oauth2: [member:transport] }]
      parameters:
        - in: query
          name: date
          schema: { type: string, format: date }
        - in: query
          name: routeId
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Viagens
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Trip' }
  /trips/{id}/reservations:
    post:
      tags: [Transport]
      summary: Reserva vaga
      operationId: createReservation
      security: [{ oauth2: [member:transport] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Vaga reservada }
        '409': { description: Lotação, duplicidade ou associado inelegível }
    delete:
      tags: [Transport]
      summary: Cancela reserva do usuário
      operationId: cancelReservation
      security: [{ oauth2: [member:transport] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '204': { description: Reserva cancelada }
  /trips/{id}/check-ins:
    post:
      tags: [Transport]
      summary: Confirma embarque
      operationId: createCheckIn
      security: [{ oauth2: [transport:checkin] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Presença registrada }
        '422': { description: QR expirado ou fora da janela }
  /meetings/{id}/rsvps:
    post:
      tags: [Meetings]
      summary: Confirma presença em reunião
      operationId: createRsvp
      security: [{ oauth2: [member:meetings] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: RSVP atualizado }
  /elections/{id}/votes:
    post:
      tags: [Elections]
      summary: Registra envelope de voto cifrado
      operationId: castVote
      security: [{ oauth2: [member:vote] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [credential, encryptedBallot]
              properties:
                credential: { type: string }
                encryptedBallot: { type: string, contentEncoding: base64 }
                proof: { type: string, contentEncoding: base64 }
      responses:
        '201': { description: Voto aceito; recibo não revela escolha }
        '409': { description: Credencial já utilizada }
        '422': { description: Prova criptográfica inválida }
  /documents/generate:
    post:
      tags: [Documents]
      summary: Solicita documento assinado
      operationId: generateDocument
      security: [{ oauth2: [member:documents] }]
      responses:
        '202': { description: Geração iniciada de forma assíncrona }
  /public/verify/{code}:
    get:
      tags: [Public]
      summary: Verifica autenticidade com exposição mínima
      operationId: verifyDocument
      security: []
      parameters:
        - in: path
          name: code
          required: true
          schema: { type: string, minLength: 8, maxLength: 64 }
      responses:
        '200':
          description: Resultado da verificação
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VerificationResult' }
        '404': { description: Código inexistente }
components:
  securitySchemes:
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.apeurn.com/oauth2/authorize
          tokenUrl: https://auth.apeurn.com/oauth2/token
          scopes:
            openid: Identificação básica
            member:benefits: Editais e inscrições próprias
            member:finance: Financeiro próprio
            member:transport: Transporte próprio
            member:meetings: Participação própria
            member:vote: Voto do eleitor apto
            member:documents: Documentos próprios
            admin:members: Gestão de associados
            admin:imports: Validação e importação de associados
            admin:finance: Gestão financeira pela diretoria autorizada
            transport:checkin: Operação de embarque
  parameters:
    ResourceId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
    Cursor:
      in: query
      name: cursor
      schema: { type: string }
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema: { type: string, minLength: 16, maxLength: 128 }
  responses:
    Conflict:
      description: Conflito com estado ou unicidade
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    ValidationError:
      description: Dados inválidos
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
  schemas:
    MembershipApplicationInput:
      type: object
      required: [name, cpf, email, phone, institution, enrollment, course, consentId]
      properties:
        name: { type: string, minLength: 3, maxLength: 150 }
        cpf: { type: string, pattern: '^\\d{11}$' }
        email: { type: string, format: email }
        phone: { type: string }
        institution: { type: string }
        enrollment: { type: string }
        course: { type: string }
        semester: { type: integer, minimum: 1, maximum: 20 }
        consentId: { type: string, format: uuid }
    MembershipApplication:
      allOf:
        - $ref: '#/components/schemas/MembershipApplicationInput'
        - type: object
          required: [id, status, createdAt]
          properties:
            id: { type: string, format: uuid }
            status: { type: string, enum: [pending, in_review, correction_requested, approved, rejected] }
            createdAt: { type: string, format: date-time }
    Notice:
      type: object
      required: [id, title, status, applicationStartsAt, applicationEndsAt]
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        status: { type: string, enum: [draft, published, applications_open, analysis, appeal, closed] }
        applicationStartsAt: { type: string, format: date-time }
        applicationEndsAt: { type: string, format: date-time }
    NoticePage:
      type: object
      required: [items]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/Notice' } }
        nextCursor: { type: [string, 'null'] }
    MinimumWagePeriodInput:
      type: object
      required: [amount, effectiveFrom, legalBasis, sourceUrl]
      properties:
        amount: { type: number, multipleOf: 0.01, minimum: 0 }
        effectiveFrom: { type: string, format: date }
        legalBasis: { type: string, maxLength: 200 }
        sourceUrl: { type: string, format: uri }
    FeePolicy:
      type: object
      required: [minimumWage, rate, monthlyAmount, effectiveFrom, legalBasis, sourceUrl]
      properties:
        minimumWage: { type: number, multipleOf: 0.01, example: 1621.00 }
        rate: { type: number, const: 0.01 }
        monthlyAmount: { type: number, multipleOf: 0.01, example: 16.21 }
        effectiveFrom: { type: string, format: date, example: '2026-01-01' }
        legalBasis: { type: string, example: 'Decreto nº 12.797/2025' }
        sourceUrl: { type: string, format: uri }
    MemberImportDiagnostic:
      type: object
      required: [totalRows, validCpfRows, duplicateCpfRows, duplicateEnrollmentRows, canImport]
      properties:
        totalRows: { type: integer, minimum: 0 }
        validCpfRows: { type: integer, minimum: 0 }
        duplicateCpfRows: { type: integer, minimum: 0 }
        duplicateEnrollmentRows: { type: integer, minimum: 0 }
        missingRequiredFields: { type: object, additionalProperties: { type: integer, minimum: 0 } }
        canImport: { type: boolean }
    Trip:
      type: object
      required: [id, routeName, departureAt, capacity, availableSeats, status]
      properties:
        id: { type: string, format: uuid }
        routeName: { type: string }
        departureAt: { type: string, format: date-time }
        capacity: { type: integer, minimum: 1 }
        availableSeats: { type: integer, minimum: 0 }
        status: { type: string, enum: [draft, open, full, boarding, in_progress, completed, delayed, cancelled] }
    VerificationResult:
      type: object
      required: [valid, status, documentType, issuedAt]
      properties:
        valid: { type: boolean }
        status: { type: string, enum: [valid, expired, revoked] }
        documentType: { type: string }
        issuedAt: { type: string, format: date-time }
        expiresAt: { type: [string, 'null'], format: date-time }
    Problem:
      type: object
      required: [type, title, status, traceId]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        traceId: { type: string }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              code: { type: string }
              message: { type: string }
