openapi: 3.0.3
info:
  title: NoLapis API Integrador SaaS
  version: 1.0.0
  description: |
    API REST para integradores SaaS (ex.: Shipper) consumirem billing, assinaturas e cobrança recorrente
    hospedados no **NoLapis ERP**.

    ## Modelo de integração

    - O integrador é um **cliente fino**: chama esta API e recebe **webhooks** outbound.
    - **Cartão de crédito**: nunca envie PAN/CVV para esta API. Use `POST /assinantes/{ref}/cartao/sessao`
      e redirecione o usuário para o **checkout hospedado** NoLapis.
    - **Gateway de pagamento**: Asaas **BYOK** — configure a API key em *Admin → Cobrança integrada*.
    - **Autenticação**: API key gerada em *Admin → API Integrador SaaS*.

    ## Fluxo típico

    1. `GET /planos` — catálogo
    2. `POST /planos` — vincular `ref` externa (opcional, se plano já existe no NoLapis)
    3. `POST /assinantes` — criar assinante + contrato recorrente
    4. `POST /assinantes/{ref}/cartao/sessao` — obter `checkout_url`
    5. Redirect do usuário para checkout NoLapis
    6. Receber webhook `cartao.associado` + redirect para `return_url`
    7. Cobranças recorrentes automáticas → webhooks `cobranca.gerada`, `cobranca.paga`, etc.

  contact:
    name: NoLapis
    url: https://nolapis.com.br
  license:
    name: Proprietary

servers:
  - url: https://api.nolapis.com.br/v1
    description: Produção — API Integrador SaaS
  - url: '{baseUrl}/v1'
    description: Ambiente configurável (NOLAPIS_API_URL)
    variables:
      baseUrl:
        default: https://api.nolapis.com.br
        description: URL base da API (sem /v1)

tags:
  - name: Planos
    description: Catálogo comercial e vínculo de referências externas
  - name: Assinantes
    description: Cadastro de assinantes finais (Pessoa + Contrato recorrente)
  - name: Cartão
    description: Checkout hospedado NoLapis (sem tokenização no integrador)
  - name: Cobranças
    description: Histórico financeiro de parcelas a receber
  - name: Contratos
    description: Upgrade, downgrade e cancelamento
  - name: Webhooks outbound
    description: |
      Eventos enviados pelo NoLapis para a URL configurada em *Admin → API Integrador SaaS*.

      **Headers enviados:**
      - `Content-Type: application/json`
      - `X-NoLapis-Event`: nome do evento (ex. `cobranca.paga`)
      - `X-NoLapis-Delivery-Id`: UUID de idempotência
      - `X-NoLapis-Signature`: `sha256={hmac}` — HMAC-SHA256 do body JSON com o webhook secret

      **Validação:** recalcule `hash_hmac('sha256', $body, $webhook_secret)` e compare com o header.

security:
  - IntegradorApiKey: []

paths:
  /planos:
    get:
      tags: [Planos]
      summary: Listar planos ativos
      description: Retorna o catálogo de planos do tenant integrador com referências externas quando vinculadas.
      operationId: listarPlanos
      responses:
        '200':
          description: Lista de planos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Plano'
              examples:
                sucesso:
                  value:
                    data:
                      - ref: pro
                        nolapis_plano_id: 3
                        codigo: pro
                        nome: Plano Pro
                        valor_mensal: 199.9
                        valor_anual: 1999.0
                        oferece_anual: true
                        status: ATIVO
        '401':
          $ref: '#/components/responses/NaoAutorizado'

    post:
      tags: [Planos]
      summary: Vincular referência externa a plano existente
      description: |
        O plano deve existir previamente no NoLapis (admin). Esta rota apenas associa
        a `ref` usada pelo integrador ao `idplano` interno.
      operationId: vincularPlano
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlanoVincularRequest'
            examples:
              vincular:
                value:
                  ref: shipper_pro
      responses:
        '200':
          description: Plano vinculado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Plano'
        '401':
          $ref: '#/components/responses/NaoAutorizado'
        '422':
          $ref: '#/components/responses/ErroValidacao'

  /assinantes:
    post:
      tags: [Assinantes]
      summary: Criar ou atualizar assinante
      description: |
        Cria/atualiza **Pessoa** (cliente) + **Contrato** de recorrência (`integrador_assinatura`).
        A mesma `ref` identifica pessoa e contrato no integrador.
      operationId: upsertAssinante
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssinanteRequest'
            examples:
              pessoa_fisica:
                value:
                  ref: tenant_shipper_12345
                  plano_ref: pro
                  cpf_cnpj: "12345678901"
                  nome: Maria Silva
                  email: maria@empresa.com
                  telefone: "11999998888"
                  periodicidade: mensal
                  dia_vencimento: 10
      responses:
        '201':
          description: Assinante sincronizado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Assinante'
        '401':
          $ref: '#/components/responses/NaoAutorizado'
        '422':
          $ref: '#/components/responses/ErroValidacao'

  /assinantes/{ref}:
    get:
      tags: [Assinantes]
      summary: Consultar assinante
      operationId: obterAssinante
      parameters:
        - $ref: '#/components/parameters/RefAssinante'
      responses:
        '200':
          description: Dados do assinante
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Assinante'
        '401':
          $ref: '#/components/responses/NaoAutorizado'
        '404':
          $ref: '#/components/responses/NaoEncontrado'

  /assinantes/{ref}/cartao/sessao:
    post:
      tags: [Cartão]
      summary: Abrir sessão de checkout de cartão
      description: |
        Retorna `checkout_url` — página **hospedada pelo NoLapis** onde o assinante cadastra o cartão.
        O integrador deve redirecionar o browser ou abrir em iframe.

        **Após sucesso:** redirect para `return_url` com query params `status=success`, `session_id`, `ref`.
        **Webhook:** `cartao.associado`.

        Checkout público (sem API key): `GET {APP_URL}/integrador/checkout/cartao/{token}`
        (domínio principal do ERP, não o subdomínio api.*)
      operationId: criarSessaoCartao
      parameters:
        - $ref: '#/components/parameters/RefAssinante'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CartaoSessaoRequest'
            examples:
              sessao:
                value:
                  return_url: https://app.shipper.com.br/assinatura/cartao-ok
                  cancel_url: https://app.shipper.com.br/assinatura/cartao-cancelado
      responses:
        '201':
          description: Sessão criada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CartaoSessao'
              examples:
                sucesso:
                  value:
                    data:
                      session_id: "42"
                      checkout_url: https://seu-dominio.nolapis.com.br/integrador/checkout/cartao/abc123...
                      expires_at: "2026-07-08T20:30:00+00:00"
        '401':
          $ref: '#/components/responses/NaoAutorizado'
        '422':
          $ref: '#/components/responses/ErroValidacao'

  /assinantes/{ref}/cobrancas:
    get:
      tags: [Cobranças]
      summary: Histórico de cobranças do assinante
      description: Últimas 50 parcelas a receber (NF) vinculadas à pessoa do assinante.
      operationId: listarCobrancasAssinante
      parameters:
        - $ref: '#/components/parameters/RefAssinante'
      responses:
        '200':
          description: Lista de cobranças
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Cobranca'
        '401':
          $ref: '#/components/responses/NaoAutorizado'
        '404':
          $ref: '#/components/responses/NaoEncontrado'

  /contratos/{ref}:
    patch:
      tags: [Contratos]
      summary: Alterar contrato (plano ou status)
      operationId: alterarContrato
      parameters:
        - $ref: '#/components/parameters/RefContrato'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContratoUpdateRequest'
            examples:
              upgrade:
                value:
                  plano_ref: enterprise
                  periodicidade: anual
              cancelar:
                value:
                  status: CANCELADO
      responses:
        '200':
          description: Contrato atualizado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Assinante'
        '401':
          $ref: '#/components/responses/NaoAutorizado'
        '422':
          $ref: '#/components/responses/ErroValidacao'

components:
  securitySchemes:
    IntegradorApiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        API key gerada em **Admin → API Integrador SaaS**.
        Formato: `nlp_live_...`

        Alternativa: header `X-NoLapis-Api-Key: {api_key}`

  parameters:
    RefAssinante:
      name: ref
      in: path
      required: true
      description: Referência externa do assinante (mesma enviada no POST /assinantes)
      schema:
        type: string
        maxLength: 128
      example: tenant_shipper_12345

    RefContrato:
      name: ref
      in: path
      required: true
      description: Referência externa do contrato (geralmente igual à ref do assinante)
      schema:
        type: string
        maxLength: 128
      example: tenant_shipper_12345

  responses:
    NaoAutorizado:
      description: API key ausente ou inválida
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'
          example:
            message: API key inválida ou integrador inativo.

    NaoEncontrado:
      description: Recurso não encontrado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'
          example:
            message: Assinante não encontrado.

    ErroValidacao:
      description: Erro de validação ou regra de negócio
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Erro'

  schemas:
    Erro:
      type: object
      properties:
        message:
          type: string
      required: [message]

    Plano:
      type: object
      properties:
        ref:
          type: string
          description: Referência externa (integrador)
        nolapis_plano_id:
          type: integer
        codigo:
          type: string
          nullable: true
        nome:
          type: string
        valor_mensal:
          type: number
          format: float
        valor_anual:
          type: number
          format: float
          nullable: true
        oferece_anual:
          type: boolean
        status:
          type: string
          example: ATIVO

    PlanoVincularRequest:
      type: object
      required: [ref]
      properties:
        ref:
          type: string
          maxLength: 128
          description: ID do plano no sistema integrador

    AssinanteRequest:
      type: object
      required: [ref, plano_ref, cpf_cnpj, nome]
      properties:
        ref:
          type: string
          maxLength: 128
          description: ID único do assinante no integrador
        plano_ref:
          type: string
          maxLength: 128
        cpf_cnpj:
          type: string
          maxLength: 20
          description: CPF ou CNPJ (somente dígitos ou formatado)
        nome:
          type: string
          maxLength: 200
        razao_social:
          type: string
          maxLength: 200
          nullable: true
        email:
          type: string
          format: email
          nullable: true
        telefone:
          type: string
          maxLength: 30
          nullable: true
        periodicidade:
          type: string
          enum: [mensal, anual]
          default: mensal
        dia_vencimento:
          type: integer
          minimum: 1
          maximum: 28
          nullable: true
        cep:
          type: string
          nullable: true
        endereco:
          type: string
          nullable: true
        numero:
          type: string
          nullable: true
        complemento:
          type: string
          nullable: true
        bairro:
          type: string
          nullable: true
        cidade:
          type: string
          nullable: true
        estado:
          type: string
          maxLength: 2
          nullable: true

    Assinante:
      type: object
      properties:
        ref:
          type: string
        nolapis_pessoa_id:
          type: integer
        nolapis_contrato_id:
          type: integer
        plano_ref:
          type: string
        plano_nome:
          type: string
        status_contrato:
          type: string
          enum: [ATIVO, PENDENTE, CANCELADO, INATIVO, TRIAL]
        periodicidade:
          type: string
          enum: [mensal, anual, trimestral, semestral]
        valor:
          type: number
          format: float
        cartao:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CartaoResumo'

    CartaoResumo:
      type: object
      properties:
        bandeira:
          type: string
          example: VISA
        final4:
          type: string
          example: "4242"
        status:
          type: string
          example: ATIVO

    CartaoSessaoRequest:
      type: object
      required: [return_url]
      properties:
        return_url:
          type: string
          format: uri
          maxLength: 500
          description: URL de retorno após cadastro do cartão
        cancel_url:
          type: string
          format: uri
          maxLength: 500
          nullable: true
          description: URL se o usuário cancelar no checkout

    CartaoSessao:
      type: object
      properties:
        session_id:
          type: string
        checkout_url:
          type: string
          format: uri
          description: Redirecionar o assinante para esta URL
        expires_at:
          type: string
          format: date-time
          description: Validade da sessão (padrão 15 minutos)

    Cobranca:
      type: object
      properties:
        nolapis_parcela_id:
          type: integer
        nolapis_nf_id:
          type: integer
        parcela:
          type: integer
        valor:
          type: number
          format: float
        vencimento:
          type: string
          format: date
          nullable: true
        status:
          type: string
          description: PENDENTE, QUITADO, etc.
        pago_em:
          type: string
          format: date-time
          nullable: true

    ContratoUpdateRequest:
      type: object
      properties:
        plano_ref:
          type: string
          maxLength: 128
        periodicidade:
          type: string
          enum: [mensal, anual]
        status:
          type: string
          enum: [ATIVO, CANCELADO, INATIVO]

    WebhookEnvelope:
      type: object
      description: Corpo JSON enviado pelo NoLapis ao webhook do integrador
      properties:
        id:
          type: string
          format: uuid
          description: ID de idempotência da entrega
        evento:
          type: string
          enum:
            - cartao.associado
            - cartao.removido
            - cobranca.gerada
            - cobranca.paga
            - cobranca.falhou
            - cobranca.vencida
            - contrato.alterado
            - assinatura.suspensa
            - assinatura.reativada
            - nfse.emitida
        criado_em:
          type: string
          format: date-time
        dados:
          type: object
          additionalProperties: true
      required: [id, evento, criado_em, dados]

    WebhookCartaoAssociado:
      allOf:
        - $ref: '#/components/schemas/WebhookEnvelope'
        - type: object
          example:
            id: 550e8400-e29b-41d4-a716-446655440000
            evento: cartao.associado
            criado_em: "2026-07-08T20:15:00+00:00"
            dados:
              ref: tenant_shipper_12345
              bandeira: VISA
              final4: "4242"
              status: ATIVO

    WebhookCobrancaPaga:
      allOf:
        - $ref: '#/components/schemas/WebhookEnvelope'
        - type: object
          example:
            id: 550e8400-e29b-41d4-a716-446655440001
            evento: cobranca.paga
            criado_em: "2026-07-08T20:20:00+00:00"
            dados:
              ref: tenant_shipper_12345
              valor: 199.9
              pago_em: "2026-07-08T20:20:00+00:00"
              forma: credit_card
              nolapis_parcela_id: 1001
              gateway_payment_id: pay_abc123

x-webhooks:
  cartao.associado:
    post:
      tags: [Webhooks outbound]
      summary: Cartão cadastrado no checkout
      description: Enviado após conclusão do checkout hospedado.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCartaoAssociado'
      responses:
        '200':
          description: Integrador confirmou recebimento

  cobranca.gerada:
    post:
      tags: [Webhooks outbound]
      summary: Nova cobrança (parcela) gerada
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - example:
                    evento: cobranca.gerada
                    dados:
                      ref: tenant_shipper_12345
                      valor: 199.9
                      vencimento: "2026-08-10"
                      nolapis_nf_id: 500
                      nolapis_parcela_id: 1001
                      gateway_payment_id: pay_abc123
      responses:
        '200':
          description: OK

  cobranca.paga:
    post:
      tags: [Webhooks outbound]
      summary: Cobrança quitada
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCobrancaPaga'
      responses:
        '200':
          description: OK

  cobranca.falhou:
    post:
      tags: [Webhooks outbound]
      summary: Falha na cobrança recorrente
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - example:
                    evento: cobranca.falhou
                    dados:
                      ref: tenant_shipper_12345
                      motivo: Pagamento recusado pelo emissor.
                      nolapis_parcela_id: 1001
      responses:
        '200':
          description: OK

  contrato.alterado:
    post:
      tags: [Webhooks outbound]
      summary: Contrato alterado (upgrade/downgrade/cancelamento)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - example:
                    evento: contrato.alterado
                    dados:
                      ref: tenant_shipper_12345
                      plano_ref: enterprise
                      status_contrato: ATIVO
                      periodicidade: anual
                      valor: 999.9
      responses:
        '200':
          description: OK

  nfse.emitida:
    post:
      tags: [Webhooks outbound]
      summary: NFS-e emitida (roadmap)
      description: Evento previsto — emissão fiscal pós-quitação. Payload sujeito a extensão.
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - example:
                    evento: nfse.emitida
                    dados:
                      ref: tenant_shipper_12345
                      numero: "12345"
                      pdf_url: https://...
                      xml_url: https://...
                      valor: 199.9
      responses:
        '200':
          description: OK

x-tagGroups:
  - name: API REST (integrador → NoLapis)
    tags: [Planos, Assinantes, Cartão, Cobranças, Contratos]
  - name: Webhooks (NoLapis → integrador)
    tags: [Webhooks outbound]
